diff --git a/.gitignore b/.gitignore index 5090528..08d15ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ -php/tokens.php -php/config.php files/* node_modules *.log .vscode/ -# NPM RC WITH SENSIBLE INFOS +# NPM RC WITH SENSITIVE INFO \.npmrc ### Node ### @@ -129,5 +127,5 @@ dist .pnp.* #jsdoc -docs/ -.DS_Store +docs/ +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7cee0..3c2d9da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,42 @@ 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.12.0] +## Unreleased + +### Added + +- Exposed `Collection.collectionName` as a readonly property for TypeScript usage. +- TypeScript overview to the README. +- Optional replacement argument for `array-splice` edit fields. +- `array-contains-none` option for array fields. +- Optional constructor for the `JSONDatabase` PHP class to reduce repetitive code. +- "Advanced" section to the README for previously undocumented features. +- `original` option for `readRaw` to not insert ID fields, for easier non-relational collection usage. + +### Changed + +- Rejected incorrect parameters are now `TypeError`s instead of regular `Error`s. +- Deprecated `firestorm.table(name)` method, since `firestorm.collection(name)` does exactly the same thing. +- Reformatted the repository and improved README.md to make it easier to set up Firestorm. +- Clean up and standardize JSDoc comments. +- `editField` and `editFieldBulk` now return confirmations like all other write methods. +- `editField` and `editFieldBulk` now reject with a descriptive error message on failure rather than silently failing. + +### Fixed + +- PHP-level errors not being rejected properly in GET requests. +- Certain write commands mutating data internally and affecting parameters outside Firestorm. +- `Collection.searchKeys` and `Collection.values` not returning proper `Error` objects sometimes. +- `files.upload` not allowing the `form-data` package's typing of `FormData` in TypeScript. +- Inconsistent use of indentation and formatting in PHP files. +- Various typos in PHP files. +- `Collection` class being exported in TypeScript despite the actual class being private. +- `array-splice` edit fields being incorrectly typed as `array-slice`. +- Platform-specific PHP error when searching nested keys. +- `Collection.remove` rejecting numeric keys, despite `Collection.removeBulk` not doing so. +- `editField` and `editFieldBulk` validation issues. + +## [1.12.0] - 2024-02-22 ### Added @@ -14,7 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Refactored JavaScript part to be less verbose and reuse existing code better. -- Use JSDoc `{@link }` properties. +- Added 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. @@ -76,4 +111,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed -- crypto module as it is now deprecated and a built-in node package +- `crypto` module as it is now deprecated and a built-in node package diff --git a/README.md b/README.md index 2ed9e78..ffa3c52 100644 --- a/README.md +++ b/README.md @@ -3,234 +3,245 @@

firestorm-db

-npm + + npm + GitHub file size in bytes - Static Badge - Tests - + + Changelog + + + Tests + -_Self hosted Firestore-like database with API endpoints based on micro bulk operations_ +*Self hosted Firestore-like database with API endpoints based on micro bulk operations* -## Installation +# Installation +Installing the JavaScript client is as simple as running: + +```sh +npm install firestorm-db ``` -npm install --save firestorm-db -``` -# JavaScript Part +Information about installing Firestorm server-side is given in the [PHP](#php-backend) section. + +# JavaScript Client -The JavaScript [index.js](./src/index.js) file is just an [Axios](https://www.npmjs.com/package/axios) wrapper of the library. +The JavaScript [index.js](./src/index.js) file is simply an [Axios](https://www.npmjs.com/package/axios) wrapper of the PHP backend. -## How to use it +## JavaScript setup -First, you need to configure your API address, and your token if needed: +First, set your API address (and your writing token if needed) using the `address()` and `token()` functions: ```js -require("dotenv").config(); // add some env variables +// only needed in Node.js, including the script tag in a browser is enough otherwise. const firestorm = require("firestorm-db"); -// ex: 'http://example.com/path/to/firestorm/root/' -firestorm.address(process.env.FIRESTORM_URL); +firestorm.address("http://example.com/path/to/firestorm/root/"); // only necessary if you want to write or access private collections // must match token stored in tokens.php file -firestorm.token(process.env.FIRESTORM_TOKEN); +firestorm.token("my_secret_token_probably_from_an_env_file"); ``` -Now you can use Firestorm to its full potential: - -```js -const firestorm = require("firestorm-db"); - -// returns a Collection instance -const userCollection = firestorm.collection("users"); +Now you can use Firestorm to its full potential. -// all methods return promises -userCollection - .readRaw() - .then((res) => console.log(res)) - .catch((err) => console.error(err)); -``` - -### Collection constructor +## Create your first collection -A collection takes one required argument and one optional argument: +Firestorm is based around the concept of a `Collection`, which is akin to an SQL table or Firestore document. The Firestorm collection constructor takes one required argument and one optional argument: -- The name of the collection as a `String`. -- 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. +- The name of the collection as a `string`. +- A 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 a queried 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"); const userCollection = firestorm.collection("users", (el) => { - el.hello = () => console.log(`${el.name} says hello!`); + // assumes you have a 'users' table with a printable field called 'name' + el.hello = () => `${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 +// all methods return promises const johnDoe = await userCollection.get(123456789); -// gives { name: "John Doe", hello: function} +// gives { name: "John Doe", hello: Function } -johnDoe.hello(); // prints out "John Doe says hello!" +johnDoe.hello(); // "John Doe says hello!" ``` -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: `{ 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. - -Not all criteria are available depending the field type. There are more options available than the firestore `where` command, allowing you to get better and faster search results. - -### All search options available - -| 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! ⚠️** | -| 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 | -| removeBulk(keys) | keys: `string[] \| number[]` | Remove multiple elements from the collection with the corresponding keys | -| set(key, value) | key: `string \| number`, value: `Object` | Sets one element with its key and value into the collection | -| setBulk(keys, values) | keys: `string[] \| number[]`, values: `Object[]` | Sets multiple elements with their corresponding keys and values into the collection | -| editField(obj) | obj: `EditObject` | Changes one field of a given element in a collection | -| editFieldBulk(objArray) | objArray: `EditObject[]` | Changes one field per element in a collection | - -### Edit field operations +## Read operations + +| Name | Parameters | Description | +| ------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| sha1() | none | Get the sha1 hash of the file. Can be used to compare file content without downloading the JSON. | +| readRaw(original) | original?: `boolean` | Read the entire collection. `original` disables ID field injection, for non-relational collections. | +| get(key) | key: `string \| number` | Get an element from the collection by its key. | +| searchKeys(keys) | keys: `string[] \| number[]` | Get multiple elements from the collection by their keys. | +| search(options, random) | options: `SearchOption[]` random?:`boolean \| number` | Search through the collection. You can randomize the output order with random as true or a given seed. | +| select(option) | option: `SelectOption` | Get only selected fields from the collection. Essentially an upgraded version of readRaw. | +| values(option) | option: `ValueOption` | 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` | Read random elements of the collection. | + +## Search options + +There are more options available than the Firestore `where` command, allowing you to get better and faster search results. + +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. Available criteria depends on the field type. + +| 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-none'` | `Array` | Entry field's array contains no values from your array | +| `'array-contains-any'` | `Array` | Entry field's array contains at least one value from your 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 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(value) | value: `Object` | Set the entire content of the collection. **⚠️ Very dangerous! ⚠️** | +| add(value) | value: `Object` | Append a value to the collection. Only works if `autoKey` is enabled server-side. | +| addBulk(values) | values: `Object[]` | Append multiple values to the collection. Only works if `autoKey` is enabled server-side. | +| remove(key) | key: `string \| number` | Remove an element from the collection by its key. | +| removeBulk(keys) | keys: `string[] \| number[]` | Remove multiple elements from the collection by their keys. | +| set(key, value) | key: `string \| number`, value: `Object` | Set a value in the collection by its key. | +| setBulk(keys, values) | keys: `string[] \| number[]`, values: `Object[]` | Set multiple values in the collection by their keys. | +| editField(obj) | option: `EditFieldOption` | Edit an element's field in the collection. | +| editFieldBulk(objArray) | options: `EditFieldOption[]` | Edit multiple elements' fields in the collection. | + +## Edit field options 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 | 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. | +| Operation | Needs value | Allowed value types | Description | +| -------------- | ----------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------| +| `set` | Yes | `any` | Sets a value to a given field. | +| `remove` | No | *N/A* | Removes a field from the element. | +| `append` | Yes | `string` | Appends a string to the end of a string field. | +| `invert` | No | *N/A* | Inverts the state of a boolean field. | +| `increment` | No | `number` | Adds a number to the field (default: 1). | +| `decrement` | No | `number` | Removes a number from the field (default: 1). | +| `array-push ` | Yes | `any` | Pushes an element to the end of an array field. | +| `array-delete` | Yes | `number` | Removes an array element by index. | +| `array-splice` | Yes | `[number, number, any?]` | Last argument is optional. Check the PHP [array_splice](https://www.php.net/manual/function.array-splice) documentation for more info. | -
+Various other methods and constants exist in the JavaScript client, which will make more sense once you learn what's actually happening behind the scenes. -# PHP Part +# PHP Backend -The PHP files are the ones handling files, read and writes. They also handle `GET` and `POST` requests to manipulate the database. +Firestorm's PHP files handle files, read, and writes, through `GET` and `POST` requests sent by the JavaScript client. All JavaScript methods correspond to an equivalent Axios request to the relevant PHP file. ## PHP setup -The developer has to create two main files at the root of their Firestorm setup: `tokens.php` and `config.php`. +The server-side files to handle requests can be found and copied to your hosting platform [here](./php/). The two files that need editing are `tokens.php` and `config.php`. -`tokens.php` will contain the tokens inside a `$db_tokens` value array with the tokens to use. You will use these tokens to write data or read private tables. - -`config.php` stores all of your collections config. You will create a `$database_list` variable with an array of `JSONDatabase` instances +- `tokens.php` contains writing tokens declared in a `$db_tokens` array. These correspond to the tokens used with `firestorm.token()` in the JavaScript client. +- `config.php` stores all of your collections. This file needs to declare a `$database_list` associative array of `JSONDatabase` instances. ```php folderPath = './files/'; -$tmp->fileName = 'users'; -$tmp->autoKey = false; +$tmp->fileName = 'orders'; +$tmp->autoKey = true; +$tmp->autoIncrement = false; $database_list[$tmp->fileName] = $tmp; -$tmp = new JSONDatabase; +// with constructor ($fileName, $autoKey = true, $autoIncrement = true) +$tmp = new JSONDatabase('users', false); $tmp->folderPath = './files/'; -$tmp->fileName = 'paths'; -$tmp->autoKey = true; $database_list[$tmp->fileName] = $tmp; -?> ``` -The database will be stored in `/.json` and `autoKey` allows or forbids some write operations. +- The database will be stored in `/.json` (default folder: `./files/`). +- `autoKey` controls whether to automatically generate the key name or to have explicit key names (default: `true`). +- `autoIncrement` controls whether to simply start generating key names from zero or to use a [random ID](https://www.php.net/manual/en/function.uniqid.php) each time (default: `true`). +- The key in the `$database_list` array is what the collection should be referred to in the JavaScript collection constructor. This can be different from the JSON filename if needed. -## Firestorm Files +If you're working with multiple collections, it's probably easier to initialize them all in the array constructor directly: -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. +```php +// config.php + new JSONDatabase('orders', true), + 'users' => new JSONDatabase('users', false), +) +``` -You have to add 2 new configuration variables to your `config.php` file: +## Permissions -```php -// whitelist of correct extensions -$authorized_file_extension = array('.txt', '.png'); +The PHP scripts used to write and read files need permissions to edit the JSON files. You can give Firestorm rights to a folder with the following command: -// subfolder of uploads location, must start with dirname($_SERVER['SCRIPT_FILENAME']) -// to force a subfolder of Firestorm installation -$STORAGE_LOCATION = dirname($_SERVER['SCRIPT_FILENAME']) . '/uploads/'; +```sh +sudo chown -R www-data "/path/to/firestorm/root/" ``` -You can use the wrapper functions in order to upload, get and delete a file. -If the folder is accessible from server url, you can directly type its address. +# Firestorm Files -### File rights +Firestorm's file APIs are implemented in `files.php`. If you don't need file-related features, then simply delete this file. -The PHP scripts create folders and files, so the script will fail if the PHP user doesn't have write permissions. -You can give rights to a folder with the following command: +To work with files server-side, you need two new configuration variables in `config.php`: +```php +// Extension whitelist +$authorized_file_extension = array('.txt', '.png', '.jpg', '.jpeg'); + +// Root directory for where files should be uploaded +// ($_SERVER['SCRIPT_FILENAME']) is a shortcut to the root Firestorm directory. +$STORAGE_LOCATION = dirname($_SERVER['SCRIPT_FILENAME']) . '/uploads/'; ``` -sudo chown -R www-data "/path/to/uploads/" -``` -### Upload a file +From there, you can use the functions in `firestorm.files` (detailed below) from the JavaScript client. + +## Upload a file -In order to upload a file, you have to give the function a `FormData` object. This class is generated from forms and is [native in modern browsers](https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData) but in Node.js can be imported with the [form-data package](https://www.npmjs.com/package/form-data). +`firestorm.files.upload` uses a `FormData` object to represent an uploaded file. This class is generated from forms and is [native in modern browsers](https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData), and with Node.js can be installed with the [form-data](https://www.npmjs.com/package/form-data) package. -The uploaded file content can be a [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob), a [Buffer](https://nodejs.org/api/buffer.html) or an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). +The uploaded file content can be a [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob), a [Buffer](https://nodejs.org/api/buffer.html), or an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). -There is additionally an overwrite option in order to avoid big mistakes and allow unique file names. +There is additionally an overwrite option in order to avoid mistakes. ```js +const FormData = require("form-data"); const firestorm = require("firestorm-db"); firestorm.address("ADDRESS_VALUE"); firestorm.token("TOKEN_VALUE"); const form = new FormData(); form.append("path", "/quote.txt"); -form.append("file", "but your kids are gonna love it.", "quote.txt"); // make sure to set a temporary name to the file -form.append("overwrite", "true"); // override optional argument (do not append to set to false) +// make sure to set a temporary file name +form.append("file", "but your kids are gonna love it.", "quote.txt"); +// override is false by default; don't append it if you don't need to +form.append("overwrite", "true"); + const uploadPromise = firestorm.files.upload(form); uploadPromise @@ -240,7 +251,7 @@ uploadPromise ## Get a file -You can get a file via its direct file URL location or its content with a request. +`firestorm.files.get` takes a file's direct URL location or its content as its parameter. If your upload folder is accessible from a server URL, you can directly use its address to retrieve the file without this method. ```js const firestorm = require("firestorm-db"); @@ -249,13 +260,13 @@ firestorm.address("ADDRESS_VALUE"); const getPromise = firestorm.files.get("/quote.txt"); getPromise - .then((fileContent) => console.log(fileContent)) // 'but your kids are gonna love it. + .then((fileContent) => console.log(fileContent)) // but your kids are gonna love it. .catch((err) => console.error(err)); ``` ## Delete a file -Because I am a nice guy, I thought about deletion too. So I figured I would put a method to delete the files too. +`firestorm.files.delete` has the same interface as `firestorm.files.get`, but as the name suggests, it deletes the file. ```js const firestorm = require("firestorm-db"); @@ -269,32 +280,182 @@ deletePromise .catch((err) => console.error(err)); ``` -## Memory warning +# TypeScript Support + +Firestorm ships with TypeScript support out of the box. -Handling big collections can cause memory allocation issues like: +## Collection types + +Collections in TypeScript take a generic parameter `T`, which is the type of each element in the collection. If you aren't using a relational collection, this can simply be set to `any`. + +```ts +import firestorm from "firestorm-db"; +firestorm.address("ADDRESS_VALUE"); +interface User { + name: string; + password: string; + pets: string[]; +} + +const userCollection = firestorm.collection("users"); + +const johnDoe = await userCollection.get(123456789); +// type: { name: string, password: string, pets: string[] } ``` -Fatal error: -Allowed memory size of 134217728 bytes exhausted (tried to allocate 32360168 bytes) + +Injected methods should also be stored in this interface. They'll get filtered out from write operations to prevent false positives: + +```ts +import firestorm from "firestorm-db"; +firestorm.address("ADDRESS_VALUE"); + +interface User { + name: string; + hello(): string; +} + +const userCollection = firestorm.collection("users", (el) => { + // interface types should agree with injected methods + el.hello = () => `${el.name} says hello!`; + return el; +}); + +const johnDoe = await userCollection.get(123456789); +const hello = johnDoe.hello(); // type: string + +await userCollection.add({ + name: "Mary Doe", + // error: 'hello' does not exist in type 'Addable'. + hello() { + return "Mary Doe says hello!" + } +}) ``` -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: +## Additional types + +Additional types exist for search criteria options, write method return types, configuration methods, the file handler, etc. + +```ts +import firestorm from "firestorm-db"; +const address = firestorm.address("ADDRESS_VALUE"); +// type: string +const deleteConfirmation = await firestorm.files.delete("/quote.txt"); +// type: firestorm.WriteConfirmation ``` -memory_limit = 256M + +# Advanced Features + +## `ID_FIELD` and its meaning + +There's a constant in Firestorm called `ID_FIELD`, which is a JavaScript-side property added afterwards to each query element. + +Its value will always be the key of the element its in, which allows you to use `Object.values` on results without worrying about losing the elements' key names. Additionally, it can be used in the method adder in the constructor, and is convenient for collections where the key name is significant. + +```js +const userCollection = firestorm.collection("users", (el) => { + el.basicInfo = () => `${el.name} (${el[firestorm.ID_FIELD]})`; + return el; +}); + +const returnedID = await userCollection.add({ name: "Bob", age: 30 }); +const returnedUser = await userCollection.get(returnedID); + +console.log(returnedID === returnedUser[firestorm.ID_FIELD]); // true + +returnedUser.basicInfo(); // Bob (123456789) +``` + +As it's entirely a JavaScript construct, `ID_FIELD` values will never be in your collection. + +## Add and set operations + +You may have noticed two different methods that seem to do the same thing: `add` and `set` (and their corresponding bulk variants). The key difference is that `add` is used on collections where `autoKey` is enabled, and `set` is used on collections where `autoKey` is disabled. `autoIncrement` doesn't affect this behavior. + +For instance, the following PHP configuration will disable add operations: + +```php +$database_list['users'] = new JSONDatabase('users', false); +``` + +```js +const userCollection = firestorm.collection("users"); +// Error: Automatic key generation is disabled +await userCollection.add({ name: "John Doe", age: 30 }); +``` + +Add operations return the generated ID of the added element, since it isn't known at add time, but set operations simply return a confirmation. If you want to get an element after it's been set, use the ID passed into the method. + +```js +// this will not work, since set operations don't return the ID +userCollection.set(123, { name: "John Doe", age: 30 }) + .then((id) => userCollection.get(id)); ``` -## API endpoints +## Combining collections -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. +Using add methods in the constructor, you can link multiple collections together. -The first keys in the request will always be the same: +```js +const orders = firestorm.collection("orders"); + +// using the example of a customer having orders +const customers = firestorm.collection("customers", (el) => { + el.getOrders = () => orders.search([ + { + field: "customer", + criteria: "==", + // assuming the customers field in the orders collection is a user ID + value: el[firestorm.ID_FIELD] + } + ]) + return el; +}) + +const johnDoe = await customers.get(123456789); + +// returns orders where the customer field is John Doe's ID +await johnDoe.getOrders(); +``` + +This functionality is particularly useful for complex data hierarchies that store fields as ID values to other collections, and is the main reason why add methods exist in the first place. It can also be used to split deeply nested data structures to increase server-side performance by only loading collections when necessary. + +## Manually sending data + +Each operation type requests a different file. In the JavaScript client, the corresponding file gets appended onto your base Firestorm address. + +- Read requests are `GET` requests sent to `/get.php`. +- Write requests are `POST` requests sent to `/post.php` with JSON data. +- File requests are sent to `/files.php` with form data. + +The first keys in a Firestorm request will always be the same regardless of its type, and further keys will depend on the specific method: ```json { - "collection": "", - "token": "", - "command": "", - ... + "collection": "", + "token": "", + "command": "", + ... } ``` + +PHP grabs the `JSONDatabase` instance created in `config.php` using the `collection` key in the request as the `$database_list` key name. From there, the `token` is used to validate the request if needed and the `command` is found and executed. + +## Memory management + +Handling very large collections can cause memory allocation issues: + +``` +Fatal error: +Allowed memory size of 134217728 bytes exhausted (tried to allocate 32360168 bytes) +``` + +If you encounter a memory allocation issue, simply change the memory limit in `/etc/php/7.4/apache2/php.ini` to be bigger: + +``` +memory_limit = 256M +``` + +If this doesn't help, considering splitting your collection into smaller collections and linking them together with methods. \ No newline at end of file diff --git a/jsdoc.json b/jsdoc.json index 05e5d96..d26b035 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -26,7 +26,7 @@ "keyword": "firebase,firestore,db,api,php,micro-operations,bulk" }, "menu": { - "Github repo": { + "Repository": { "href": "https://github.com/TheRolfFR/firestorm", "target": "_blank", "class": "menu-item", diff --git a/package.json b/package.json index 7f7a10f..4a567ee 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "full": "npm run php_stop ; npm run php_start && npm run test ; npm run php_stop", "local_tests": "sudo act -P ubuntu-latest=shivammathur/node:latest -j js-wrapper-test", "jsdoc": "jsdoc src/index.js -c jsdoc.json -R README.md -t ./node_modules/docdash -d out", - "nodemon_jsdoc": "nodemon -x npm run jsdoc --watch src/index.js --watch jsdoc.json", + "nodemon_jsdoc": "nodemon -x npm run jsdoc --watch src/index.js --watch jsdoc.json --watch README.md", "types": "npx tsc", "prettier": "prettier \"{,!(node_modules)/**/}*.{js,ts}\" --config .prettierrc --write", "cov": "npm run php_stop ; npm run php_start && nyc --reporter=text mocha tests/**/*.spec.js; npm run php_stop" @@ -39,19 +39,19 @@ "typings/index.d.ts" ], "dependencies": { - "axios": "^1.6.7" + "axios": "^1.6.8", + "form-data": "^4.0.0" }, "devDependencies": { "chai": "^4.4.1", "docdash": "^2.0.2", - "form-data": "^4.0.0", - "glob": "^10.3.10", + "glob": "^10.3.12", "jsdoc": "^4.0.2", "jsdoc-to-markdown": "^8.0.1", - "mocha": "^10.3.0", + "mocha": "^10.4.0", "nyc": "^15.1.0", "prettier": "^3.2.5", "recursive-copy": "^2.0.14", - "typescript": "^5.3.3" + "typescript": "^5.4.5" } } diff --git a/src/.htaccess b/php/.htaccess similarity index 100% rename from src/.htaccess rename to php/.htaccess diff --git a/php/classes/FileAccess.php b/php/classes/FileAccess.php new file mode 100644 index 0000000..4e6b038 --- /dev/null +++ b/php/classes/FileAccess.php @@ -0,0 +1,61 @@ + $filepath, 'content' => ''); + // open file as binary + $file = fopen($filepath, 'rb'); + + // exit if couldn't find file + if ($file === false) { + if ($default == null) + throw new Exception("Could not open file: $filepath"); + + // set default value + $fileObj['content'] = $default; + } + + // if no file, puts default value inside + if ($file === false) { + file_put_contents($fileObj['filepath'], $fileObj['content'], LOCK_EX); + $file = fopen($filepath, 'rb'); + } + + $fileObj['fd'] = $file; + + // if want the lock, we wait for the shared lock + if ($waitLock) { + $lock = flock($file, LOCK_SH); + if (!$lock) { + fclose($file); + throw new Exception('Failed to lock file'); + } + } + + // read file content + $string = ''; + while (!feof($file)) { + $string .= fread($file, 8192); + } + + $fileObj['content'] = $string; + + // if no wait you can close the file + if (!$waitLock) + fclose($file); + + return $fileObj; + } + public static function write($fileObj) { + // lock and close + flock($fileObj['fd'], LOCK_UN); + fclose($fileObj['fd']); + + if (!is_writable($fileObj['filepath'])) { + throw new HTTPException("PHP script can't write to file. Check permission, group and owner.", 400); + } + + $ret = file_put_contents($fileObj['filepath'], $fileObj['content'], LOCK_EX); + return $ret; + } +} diff --git a/php/classes/HTTPException.php b/php/classes/HTTPException.php new file mode 100644 index 0000000..68f3a57 --- /dev/null +++ b/php/classes/HTTPException.php @@ -0,0 +1,23 @@ +code}]: {$this->message}\n"; + } +} diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php new file mode 100644 index 0000000..6f26a8f --- /dev/null +++ b/php/classes/JSONDatabase.php @@ -0,0 +1,705 @@ +fileName = $fileName; + $this->autoKey = $autoKey; + $this->autoIncrement = $autoIncrement; + } + + 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 cannot be a $content_type", 400); + } + + // value must not be a sequential array with values inside [1, 2, 3] + // we accept sequential arrays but with objects not primitives + if (is_array($content) and !array_assoc($content)) { + foreach ($content as $item) { + $item_type = gettype($item); + if (in_array($item_type, $incorrect_types)) { + throw new HTTPException("write_raw item cannot be a $item_type", 400); + } + } + } + + // now we know we have an associative array + + // content must be objects + foreach ($content as $key => $item) { + // item must not be primitive + + // we don't accept primitive keys as value + $item_type = gettype($item); + if (in_array($item_type, $incorrect_types)) { + throw new HTTPException("write_raw item with key $key cannot be a $item_type", 400); + } + + // we accept associative array as items because they may have an integer key + } + + $content = stringifier($content); + + // fix empty raw content because 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); + } + + public function sha1() { + $obj = $this->read_raw(); + return sha1($obj['content']); + } + + public function read_raw($waitLock = false) { + // fall back to empty array if failed + return FileAccess::read($this->fullPath(), $waitLock, json_encode(array())); + } + + 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) { + // "===" fixes the empty array "==" comparison + if ($key === null or $value === null) { + throw new HTTPException('Key or value is null', 400); + } + + $key_var_type = gettype($key); + if (!is_keyable($key)) + throw new HTTPException("Incorrect key type, got $key_var_type, expected string or integer", 400); + + $value_var_type = gettype($value); + if (is_primitive($value)) + 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); + + // 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 HTTPException('Incorrect 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); + + // regular for loop to join keys and values together + for ($i = 0; $i < count($value_decoded); $i++) { + $key_var_type = gettype($keys_decoded[$i]); + if (!is_keyable($keys_decoded[$i])) + throw new HTTPException("Incorrect key type, got $key_var_type, expected string or integer"); + + $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'); + sort($int_keys); + $last_key = count($int_keys) > 0 ? $int_keys[count($int_keys) - 1] + 1 : 0; + } else { + $last_key = uniqid(); + while (array_key_exists($last_key, $arr)) + $last_key = uniqid(); + } + + return strval($last_key); + } + + public function add($value) { + if ($this->autoKey == false) + throw new HTTPException('Automatic key generation is disabled'); + + // restricts types to objects only + $value_type = gettype($value); + if (is_primitive($value) or (is_array($value) and count($value) and !array_assoc($value))) + throw new HTTPException("add value must be an object, not a $value_type", 400); + + // 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 (!$this->autoKey) + throw new HTTPException('Automatic key generation is disabled'); + + if ($values !== array() and $values == NULL) + throw new HTTPException('null-like value not accepted', 400); + + // restricts types to non base variables + $value_type = gettype($values); + if (is_primitive($values) or (is_array($values) and count($values) and array_assoc($values))) + throw new HTTPException("value must be an array not a $value_type", 400); + + // so here we have a sequential array type + // now the values inside this array must not be base values + foreach ($values as $value) { + $value_type = gettype($value); + if (is_primitive($value) or (array_sequential($value) and count($value))) + throw new HTTPException("array value must be an object not a $value_type", 400); + } + + // verify that values is an array with number indices + if (array_assoc($values)) + throw new HTTPException('Wanted sequential array'); + + // else set it at the corresponding 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) { + $key_var_type = gettype($key); + if (!is_keyable($key)) + throw new HTTPException("Incorrect key type, got $key_var_type, expected string or integer", 400); + + $obj = $this->read(true); + 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); + + for ($i = 0; $i < count($keys); $i++) { + $key_var_type = gettype($keys[$i]); + if (!is_keyable($keys[$i])) + throw new HTTPException("Incorrect key type, got $key_var_type, expected string or integer", 400); + else + $keys[$i] = strval($keys[$i]); + } + + $obj = $this->read(true); + + // remove all keys + foreach ($keys as $key_decoded) + unset($obj['content'][$key_decoded]); + + $this->write($obj); + } + + private function _search($field, $criteria, $value, $ignoreCase): bool { + $fieldType = gettype($field); + switch ($fieldType) { + case 'boolean': + switch ($criteria) { + case '!=': + return $field != $value; + case '==': + return $field == $value; + default: + return false; + } + case 'integer': + case 'double': + switch ($criteria) { + case '!=': + return $field != $value; + case '==': + return $field == $value; + case '>=': + return $field >= $value; + case '<=': + return $field <= $value; + case '<': + return $field < $value; + case '>': + return $field > $value; + case 'in': + return in_array($field, $value); + default: + return false; + } + case 'string': + // saves a lot of duplicate ternaries, no idea why php needs these to be strings + $cmpFunc = $ignoreCase ? 'strcasecmp' : 'strcmp'; + $posFunc = $ignoreCase ? 'stripos' : 'strpos'; + switch ($criteria) { + case '!=': + return $cmpFunc($field, $value) != 0; + case '==': + return $cmpFunc($field, $value) == 0; + case '>=': + return $cmpFunc($field, $value) >= 0; + case '<=': + return $cmpFunc($field, $value) <= 0; + case '<': + return $cmpFunc($field, $value) < 0; + case '>': + return $cmpFunc($field, $value) > 0; + case 'includes': + case 'contains': + return $value != '' ? ($posFunc($field, $value) !== false) : true; + case 'startsWith': + return $value != '' ? ($posFunc($field, $value) === 0) : true; + case 'endsWith': + $end = substr($field, -strlen($value)); + return $value != '' ? ($cmpFunc($end, $value) === 0) : true; + case 'in': + $found = false; + foreach ($value as $val) { + $found = $cmpFunc($field, $val) == 0; + if ($found) + break; + } + return $found; + default: + return false; + } + case 'array': + switch ($criteria) { + case 'array-contains': + return array_contains($field, $value, $ignoreCase); + case 'array-contains-none': + return !array_contains_any($field, $value, $ignoreCase); + case 'array-contains-any': + return array_contains_any($field, $value, $ignoreCase); + case 'array-length': + case 'array-length-eq': + return count($field) == $value; + case 'array-length-df': + return count($field) != $value; + case 'array-length-gt': + return count($field) > $value; + case 'array-length-lt': + return count($field) < $value; + case 'array-length-ge': + return count($field) >= $value; + case 'array-length-le': + return count($field) <= $value; + default: + return false; + } + default: + break; + } + + // unknown type + return false; + } + + public function search($conditions, $random = false) { + $obj = $this->read(); + $res = []; + + foreach ($obj['content'] as $key => $el) { + $el_root = $el; + + $add = true; + foreach ($conditions as $condition) { + if (!$add) + break; + + // extract field + $field = $condition['field']; + $field_path = explode('.', $field); + + // get nested fields if needed + for ($field_ind = 0; $el != NULL && $field_ind + 1 < count($field_path); ++$field_ind) { + // don't crash if unknown nested key, break early + if (!array_key_exists($field_path[$field_ind], $el)) + break; + + $el = $el[$field_path[$field_ind]]; + $field = $field_path[$field_ind + 1]; + } + + if ( + $el == NULL || + !array_key_exists($field, $el) || + !array_key_exists('criteria', $condition) || + !array_key_exists('value', $condition) + ) { + $add = false; + break; + } + + $ignoreCase = array_key_exists('ignoreCase', $condition) && !!$condition['ignoreCase']; + $add = $this->_search( + $el[$field], + $condition['criteria'], + $condition['value'], + $ignoreCase + ); + + $el = $el_root; + } + + // if all conditions are met, we can add the value to our output + if ($add) + $res[$key] = $el_root; + } + + if ($random !== false) { + $seed = false; + if (is_array($random) && array_key_exists('seed', $random)) { + $rawSeed = sec($random['seed']); + if (!is_int($rawSeed)) + throw new HTTPException('Seed not an integer value for random search result'); + $seed = intval($rawSeed); + } + $res = choose_random($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] = $obj['content'][$key]; + } + } + + return $res; + } + + // MANDATORY REFERENCE to edit directly: PHP 5+ + private function _edit(&$obj, $editObj): bool { + // must be associative array + $editObjType = gettype($editObj); + if (is_primitive($editObj) || array_sequential($editObj)) + throw new HTTPException("Edit object has wrong type $editObjType, expected object", 400); + + // id required + if (!array_key_exists('id', $editObj) || !check($editObj['id'])) + throw new HTTPException('Missing ID field', 400); + + $id = $editObj['id']; + + // id string or integer + if (!is_keyable($id)) + throw new HTTPException('ID must be a string or number', 400); + + // object not found + if (!array_key_exists($id, $obj['content']) || !check($obj['content'][$id])) + throw new HTTPException('ID doesn\'t exist in collection', 400); + + // field required + if (!array_key_exists('field', $editObj) || !check($editObj['field'])) + throw new HTTPException('Missing field field', 400); + + $field = $editObj['field']; + + // field is a string + if (gettype($field) != 'string') + throw new HTTPException('field must be a string', 400); + + // operation required + if (!array_key_exists('operation', $editObj) || !check($editObj['operation'])) + throw new HTTPException('Missing operation field', 400); + + $operation = $editObj['operation']; + + $value = null; + + // return if operation has no value + // set, append, array-push, array-delete, array-splice + if ( + in_array($operation, ['set', 'append', 'array-push', 'array-delete', 'array-splice']) and + (!array_key_exists('value', $editObj) or !isset($editObj['value'])) + ) + throw new HTTPException("A value is required for operation $operation", 400); + else if (array_key_exists('value', $editObj)) + $value = $editObj['value']; + + // field not needed for set or push operation (can create fields) + // missing field in remove doesn't matter since it's gone either way + if ( + !isset($obj['content'][$id][$field]) and + ($operation != 'set' and $operation != 'remove' and $operation != 'array-push') + ) + throw new HTTPException("Field $field doesn't exist in ID $id", 400); + + switch ($operation) { + case 'set': + $obj['content'][$id][$field] = $value; + return true; + case 'remove': + unset($obj['content'][$id][$field]); + return true; + case 'append': + // check type string + if (gettype($obj['content'][$id][$field]) != 'string' or gettype($value) != 'string') + throw new HTTPException('append requires string values', 400); + + $obj['content'][$id][$field] .= $value; + return true; + case 'invert': + // check type boolean + if (gettype($obj['content'][$id][$field]) != 'boolean') + throw new HTTPException('invert field must be a boolean', 400); + + $obj['content'][$id][$field] = !$obj['content'][$id][$field]; + return true; + case 'increment': + case 'decrement': + // check type number + if (!is_number_like($obj['content'][$id][$field])) + throw new HTTPException('increment and decrement fields must be numbers', 400); + + $change = $operation == 'increment' ? 1 : -1; + + // check if value + if (isset($editObj['value'])) { + // error here + if (is_number_like($editObj['value'])) + $change *= $editObj['value']; + // incorrect value provided, no operation done + else + throw new HTTPException('increment and decrement values must be numbers', 400); + } + + $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' || + array_assoc($obj['content'][$id][$field]) + ) + throw new HTTPException('array-push field must be an array', 400); + + array_push($obj['content'][$id][$field], $value); + + return true; + + case 'array-delete': + // check if our field array + if ( + gettype($obj['content'][$id][$field]) != 'array' || + array_assoc($obj['content'][$id][$field]) + ) + throw new HTTPException('array-delete field must be an array', 400); + + // value must be integer + if (gettype($value) != 'integer') + throw new HTTPException('array-delete value must be a number', 400); + + array_splice($obj['content'][$id][$field], $value, 1); + + return true; + case 'array-splice': + if (array_assoc($obj['content'][$id][$field])) + throw new HTTPException('array-splice field must be an array', 400); + + // value must be an array starting with two integers + if ( + array_assoc($value) or + count($value) < 2 or + gettype($value[0]) != 'integer' or + gettype($value[1]) != 'integer' + ) + throw new HTTPException('Incorrect array-splice options', 400); + + if (count($value) > 2) + array_splice($obj['content'][$id][$field], $value[0], $value[1], $value[2]); + else + array_splice($obj['content'][$id][$field], $value[0], $value[1]); + + return true; + default: + break; + } + + throw new HTTPException("Unknown operation $operation", 400); + } + + public function editField($editObj) { + $fileObj = $this->read(true); + $this->_edit($fileObj, $editObj); + $this->write($fileObj); + } + + public function editFieldBulk($objArray) { + // need sequential array + if (array_assoc($objArray)) + return false; + + $fileObj = $this->read(true); + foreach ($objArray as &$editObj) { + // edit by reference, faster than passing values back and forth + $this->_edit($fileObj, $editObj); + } + $this->write($fileObj); + } + + public function select($selectObj) { + if (!array_key_exists('fields', $selectObj)) + throw new HTTPException('Missing required fields field'); + + if (!gettype($selectObj['fields']) === 'array' || !array_sequential($selectObj['fields'])) + throw new HTTPException('Incorrect fields type, expected an array'); + + // all field arguments should be strings + $fields = $selectObj['fields']; + foreach ($fields as $field) { + if (gettype($field) !== 'string') + throw new HTTPException('fields field incorrect, expected a string array'); + } + + $obj = $this->read(); + + $result = array(); + foreach ($obj['content'] as $key => $value) { + $result[$key] = array(); + foreach ($fields as $field) { + if (array_key_exists($field, $value)) + $result[$key][$field] = $value[$field]; + } + } + + 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(); + + $result = []; + foreach ($obj['content'] as $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/php/classes/read/random.php b/php/classes/read/random.php new file mode 100644 index 0000000..4909ccf --- /dev/null +++ b/php/classes/read/random.php @@ -0,0 +1,85 @@ += -1 for the max'); + + $hasSeed = array_key_exists('seed', $params); + $hasOffset = array_key_exists('offset', $params); + + // offset is relevant only if you get the key + if ($hasOffset && !$hasSeed) + throw new HTTPException('You can\'t put an offset without a seed'); + + // offset validation + $offset = $hasOffset ? $params['offset'] : 0; + if ($hasOffset && (gettype($offset) !== 'integer' || $offset < 0)) + throw new HTTPException('Expected integer >= 0 for the offset'); + + // seed validation + $seed = $hasSeed ? $params['seed'] : false; + if ($hasSeed && gettype($seed) !== 'integer') + throw new HTTPException('Expected integer for the seed'); + + $json = $class->read()['content']; + + return choose_random($json, $seed, $max, $offset); +} + +function choose_random($json, $seed = false, $max = -1, $offset = 0) { + $keys = array_keys($json); + $keys_selected = array(); + $keys_length = count($keys); + + // return an empty array, can't get more elements + if ($offset >= $keys_length) return array(); + + if ($max == -1 || $max > $keys_length) $max = $keys_length; + + // set random seed just before starting picking + if ($seed !== false) mt_srand($seed); + + // the thing is that I need to splice keys from before the offset + for ($i = 0; $i < $offset; ++$i) { + $index = mt_rand(0, $keys_length - 1); + array_splice($keys, $index, 1); + + // update keys length + $keys_length = count($keys); + } + + // then while I can get new entries + // -> I still have keys + // -> I am not at maximum + for ($i = 0; $keys_length > 0 && $i < $max; ++$i) { + // get an index + $index = mt_rand(0, $keys_length - 1); + + // move element to keys selected + $keys_selected = array_merge($keys_selected, array_splice($keys, $index, 1)); + + // recompute keys left + $keys_length = count($keys); + } + + // get objects from keys selected + $result = array(); + foreach ($keys_selected as $k) { + $key = strval($k); + $result[$key] = $json[$key]; + } + + return $result; +} diff --git a/src/php/classes/read/searchArray.php b/php/classes/read/searchArray.php similarity index 83% rename from src/php/classes/read/searchArray.php rename to php/classes/read/searchArray.php index 19d0920..61fec0d 100644 --- a/src/php/classes/read/searchArray.php +++ b/php/classes/read/searchArray.php @@ -1,12 +1,11 @@ fileName = 'my_json_name'; +// Whether to automatically generate the key name or to have explicit key names +// - Default: true +$db->autoKey = true; +// Whether to simply start at 0 and increment or to use a random ID name +// - Ignored if autoKey is false +// - Default: true +$db->autoIncrement = true; +// The database_list key is what the collection will be called in JavaScript +$database_list['my_collection_name'] = $db; + +// This can be simplified into the following constructor: +// - Note: all of these arguments are optional and will fall back to their defaults if not provided +// - Order: (fileName, autoKey, autoIncrement) +$database_list['my_collection_name'] = new JSONDatabase('my_json_name', true, true); + +/** + * File handling: + * If you don't need this functionality, delete this section and files.php. + */ + +// Extension whitelist +$authorized_file_extension = array('.txt', '.png', '.jpg', '.jpeg'); + +// Root directory for where files should be uploaded +// ($_SERVER['SCRIPT_FILENAME']) is a shortcut to the root Firestorm directory. +$STORAGE_LOCATION = dirname($_SERVER['SCRIPT_FILENAME']) . '/uploads/'; diff --git a/src/php/error.php b/php/error.php similarity index 100% rename from src/php/error.php rename to php/error.php diff --git a/php/files.php b/php/files.php new file mode 100644 index 0000000..9ead278 --- /dev/null +++ b/php/files.php @@ -0,0 +1,208 @@ +sha1(); http_response($res); @@ -72,12 +75,12 @@ $id = check_key_json('id', $inputJSON); // strict compare to include 0 or "0" ids - if($id === false) + if ($id === false) http_error(400, 'No id provided'); $result = $db->get($id); - if(!$result) - http_error(404, 'get failed on collection ' . $collection . ' with key ' . $id); + if (!$result) + http_error(404, "get failed on collection $collection with key $id"); http_response(stringifier($result)); break; @@ -85,7 +88,7 @@ $search = check_key_json('search', $inputJSON, false); $random = check_key_json('random', $inputJSON, false); - if(!$search) + if (!$search) http_error(400, 'No search provided'); $result = $db->search($search, $random); @@ -95,7 +98,7 @@ case 'searchKeys': $search = check_key_json('search', $inputJSON, false); - if(!$search) + if (!$search) http_error(400, 'No search provided'); $result = $db->searchKeys($search); @@ -105,20 +108,20 @@ case 'select': $select = check_key_json('select', $inputJSON, false); - if($select === false) http_error('400', 'No select provided'); + if ($select === false) http_error('400', 'No select provided'); $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'); + 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'); + if ($params === false) http_error('400', 'No random object provided'); http_response(stringifier($db->random($params))); default: diff --git a/src/php/index.php b/php/index.php similarity index 57% rename from src/php/index.php rename to php/index.php index c6d608d..283a159 100644 --- a/src/php/index.php +++ b/php/index.php @@ -1,6 +1,6 @@ format('Y-m-d H:i:s')); fwrite($fp, $message); - fwrite($fp, '\n'); - fclose($fp); + fwrite($fp, '\n'); + fclose($fp); } -} \ No newline at end of file +} diff --git a/src/php/post.php b/php/post.php similarity index 54% rename from src/php/post.php rename to php/post.php index 47cbf7e..43e6335 100644 --- a/src/php/post.php +++ b/php/post.php @@ -1,79 +1,86 @@ write_raw($value); - http_success('Successful ' . $command . ' command'); + http_success("Successful $command command"); break; case 'add': $newId = $db->add($value); @@ -85,50 +92,50 @@ break; case 'remove': $db->remove($value); - http_success('Successful ' . $command . ' command'); + http_success("Successful $command command"); break; case 'removeBulk': $db->removeBulk($value); - http_success('Successful ' . $command . ' command'); + http_success("Successful $command command"); break; case 'set': $dbKey = check_key_json('key', $inputJSON); - if($dbKey === false) + if ($dbKey === false) http_error(400, 'No key provided'); - + $db->set($dbKey, $value); - http_success('Successful ' . $command . ' command'); + http_success("Successful $command command"); break; case 'setBulk': $dbKey = check_key_json('keys', $inputJSON, false); - if($dbKey === false) + if ($dbKey === false) http_error(400, 'No keys provided'); - + $db->setBulk($dbKey, $value); - http_success('Successful ' . $command . ' command'); + http_success("Successful $command command"); break; case 'editField': $res = $db->editField($value); - if($res === false) + if ($res === false) http_error(400, 'Incorrect data provided'); - - http_message($res, 'success', 200); + + http_success("Successful $command command"); break; case 'editFieldBulk': $res = $db->editFieldBulk($value); - if($res === false) + if ($res === false) http_error(400, 'Incorrect data provided'); - - http_message($res, 'success', 200); + + http_success("Successful $command command"); break; default: break; } -http_error(404, 'No request handler found for command ' . $command); +http_error(404, "No request handler found for command $command"); } catch(HTTPException $e) { http_error($e->getCode(), $e->getMessage()); } catch(Exception $e) { http_error(400, $e->getMessage()); -} \ No newline at end of file +} diff --git a/php/tokens.php b/php/tokens.php new file mode 100644 index 0000000..74955ec --- /dev/null +++ b/php/tokens.php @@ -0,0 +1,3 @@ +'; + var_dump($val); + echo ''; +} + +function check($var) { + return isset($var) and !empty($var); +} + +function sec($var) { + return htmlspecialchars($var); +} + +function http_response($body, $code = 200) { + header('Content-Type: application/json'); + http_response_code($code); + echo $body; + + exit(); +} + +function http_json_response($json, $code = 200) { + http_response(json_encode($json), $code); +} + +function http_message($message, $key = 'message', $code = 200) { + $arr = array($key => $message); + http_json_response($arr, $code); +} + +function http_error($code, $message) { + http_message($message, 'error', $code); +} + +function is_primitive($value) { + $value_type = gettype($value); + return $value_type == 'NULL' || + $value_type == 'boolean' || + $value_type == 'integer' || + $value_type == 'double' || + $value_type == 'string'; +} + +function is_number_like($value) { + $value_type = gettype($value); + return in_array($value_type, ['integer', 'double']); +} + +function is_keyable($value) { + return in_array(gettype($value), ['integer', 'string']); +} + +function http_success($message) { + http_message($message, 'message', 200); +} + +function check_key_json($key, $arr, $parse = false) { + if (array_key_exists($key, $arr)) + return $parse ? sec($arr[$key]) : $arr[$key]; + return false; +} + +function array_assoc($arr) { + if (array() === $arr || !is_array($arr)) return false; + return array_keys($arr) !== range(0, count($arr) - 1); +} + +function array_sequential($arr) { + return !array_assoc($arr); +} + +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; +} + +function cors() { + // Allow from any origin + if (isset($_SERVER['HTTP_ORIGIN'])) { + header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}"); + header('Access-Control-Allow-Credentials: true'); + header('Access-Control-Max-Age: 86400'); // cache for 1 day + } + + // Access-Control headers are received during OPTIONS requests + if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { + + if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) + 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']}"); + + exit(0); + } +} + +function remove_dots($path) { + $root = ($path[0] === '/') ? '/' : ''; + + $segments = explode('/', trim($path, '/')); + $ret = array(); + foreach ($segments as $segment) { + if ($segment == '.' || strlen($segment) === 0) continue; + if ($segment == '..') array_pop($ret); + else array_push($ret, $segment); + } + return $root . implode('/', $ret); +} + +if (!function_exists('str_ends_with')) { + function str_ends_with(string $haystack, string $needle): bool { + $needle_len = strlen($needle); + return $needle_len === 0 || 0 === substr_compare($haystack, $needle, -$needle_len); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b0d92a..60b239e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,11 @@ settings: dependencies: axios: - specifier: ^1.6.7 - version: 1.6.7 + specifier: ^1.6.8 + version: 1.6.8 + form-data: + specifier: ^4.0.0 + version: 4.0.0 devDependencies: chai: @@ -16,12 +19,9 @@ devDependencies: docdash: specifier: ^2.0.2 version: 2.0.2 - form-data: - specifier: ^4.0.0 - version: 4.0.0 glob: - specifier: ^10.3.10 - version: 10.3.10 + specifier: ^10.3.12 + version: 10.3.12 jsdoc: specifier: ^4.0.2 version: 4.0.2 @@ -29,8 +29,8 @@ devDependencies: specifier: ^8.0.1 version: 8.0.1 mocha: - specifier: ^10.3.0 - version: 10.3.0 + specifier: ^10.4.0 + version: 10.4.0 nyc: specifier: ^15.1.0 version: 15.1.0 @@ -41,46 +41,46 @@ devDependencies: specifier: ^2.0.14 version: 2.0.14 typescript: - specifier: ^5.3.3 - version: 5.3.3 + specifier: ^5.4.5 + version: 5.4.5 packages: - /@ampproject/remapping@2.2.1: - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.22 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 dev: true - /@babel/code-frame@7.23.5: - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + /@babel/code-frame@7.24.2: + resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 + '@babel/highlight': 7.24.2 + picocolors: 1.0.0 dev: true - /@babel/compat-data@7.23.5: - resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + /@babel/compat-data@7.24.4: + resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} engines: {node: '>=6.9.0'} dev: true - /@babel/core@7.23.9: - resolution: {integrity: sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==} + /@babel/core@7.24.4: + resolution: {integrity: sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==} engines: {node: '>=6.9.0'} dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.4 '@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 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) + '@babel/helpers': 7.24.4 + '@babel/parser': 7.24.4 + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 convert-source-map: 2.0.0 debug: 4.3.4(supports-color@8.1.1) gensync: 1.0.0-beta.2 @@ -90,13 +90,13 @@ packages: - supports-color dev: true - /@babel/generator@7.23.6: - resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + /@babel/generator@7.24.4: + resolution: {integrity: sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.9 - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.22 + '@babel/types': 7.24.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 dev: true @@ -104,7 +104,7 @@ packages: resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/compat-data': 7.23.5 + '@babel/compat-data': 7.24.4 '@babel/helper-validator-option': 7.23.5 browserslist: 4.23.0 lru-cache: 5.1.1 @@ -120,33 +120,33 @@ packages: resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.23.9 - '@babel/types': 7.23.9 + '@babel/template': 7.24.0 + '@babel/types': 7.24.0 dev: true /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.9 + '@babel/types': 7.24.0 dev: true - /@babel/helper-module-imports@7.22.15: - resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + /@babel/helper-module-imports@7.24.3: + resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.23.9 + '@babel/types': 7.24.0 dev: true - /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.9): + /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4): resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/core': 7.23.9 + '@babel/core': 7.24.4 '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.22.15 + '@babel/helper-module-imports': 7.24.3 '@babel/helper-simple-access': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 @@ -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.9 + '@babel/types': 7.24.0 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.9 + '@babel/types': 7.24.0 dev: true - /@babel/helper-string-parser@7.23.4: - resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + /@babel/helper-string-parser@7.24.1: + resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} engines: {node: '>=6.9.0'} dev: true @@ -181,66 +181,67 @@ packages: engines: {node: '>=6.9.0'} dev: true - /@babel/helpers@7.23.9: - resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} + /@babel/helpers@7.24.4: + resolution: {integrity: sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/template': 7.23.9 - '@babel/traverse': 7.23.9 - '@babel/types': 7.23.9 + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 transitivePeerDependencies: - supports-color dev: true - /@babel/highlight@7.23.4: - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + /@babel/highlight@7.24.2: + resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} engines: {node: '>=6.9.0'} dependencies: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 + picocolors: 1.0.0 dev: true - /@babel/parser@7.23.9: - resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} + /@babel/parser@7.24.4: + resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.23.9 + '@babel/types': 7.24.0 dev: true - /@babel/template@7.23.9: - resolution: {integrity: sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==} + /@babel/template@7.24.0: + resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.23.5 - '@babel/parser': 7.23.9 - '@babel/types': 7.23.9 + '@babel/code-frame': 7.24.2 + '@babel/parser': 7.24.4 + '@babel/types': 7.24.0 dev: true - /@babel/traverse@7.23.9: - resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} + /@babel/traverse@7.24.1: + resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.4 '@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.9 - '@babel/types': 7.23.9 + '@babel/parser': 7.24.4 + '@babel/types': 7.24.0 debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color dev: true - /@babel/types@7.23.9: - resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} + /@babel/types@7.24.0: + resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.23.4 + '@babel/helper-string-parser': 7.24.1 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 dev: true @@ -273,13 +274,13 @@ packages: engines: {node: '>=8'} dev: true - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} dependencies: - '@jridgewell/set-array': 1.1.2 + '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.22 + '@jridgewell/trace-mapping': 0.3.25 dev: true /@jridgewell/resolve-uri@3.1.2: @@ -287,8 +288,8 @@ packages: engines: {node: '>=6.0.0'} dev: true - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} dev: true @@ -296,15 +297,15 @@ packages: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} dev: true - /@jridgewell/trace-mapping@0.3.22: - resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@jsdoc/salty@0.2.7: - resolution: {integrity: sha512-mh8LbS9d4Jq84KLw8pzho7XC2q2/IJGiJss3xwRoLD1A+EE16SjN4PfaG4jRCzKegTFLlN0Zd8SdUPE6XdoPFg==} + /@jsdoc/salty@0.2.8: + resolution: {integrity: sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==} engines: {node: '>=v12.0.0'} dependencies: lodash: 4.17.21 @@ -476,9 +477,10 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false - /axios@1.6.7: - resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} + /axios@1.6.8: + resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} dependencies: follow-redirects: 1.15.6 form-data: 4.0.0 @@ -491,8 +493,8 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} dev: true @@ -529,8 +531,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001588 - electron-to-chromium: 1.4.677 + caniuse-lite: 1.0.30001610 + electron-to-chromium: 1.4.739 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) dev: true @@ -564,8 +566,8 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite@1.0.30001588: - resolution: {integrity: sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==} + /caniuse-lite@1.0.30001610: + resolution: {integrity: sha512-QFutAY4NgaelojVMjY63o6XlZyORPaLfyMnsl3HgnWdJUcX6K0oaJymHjH8PT5Gk7sTm8rvC/c5COUQKXqmOMA==} dev: true /catharsis@0.9.0: @@ -681,6 +683,7 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 + dev: false /command-line-args@5.2.1: resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} @@ -794,6 +797,7 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + dev: false /diff@5.0.0: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} @@ -821,15 +825,15 @@ packages: /docdash@2.0.2: resolution: {integrity: sha512-3SDDheh9ddrwjzf6dPFe1a16M6ftstqTNjik2+1fx46l24H9dD2osT2q9y+nBEC1wWz4GIqA48JmicOLQ0R8xA==} dependencies: - '@jsdoc/salty': 0.2.7 + '@jsdoc/salty': 0.2.8 dev: true /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true - /electron-to-chromium@1.4.677: - resolution: {integrity: sha512-erDa3CaDzwJOpyvfKhOiJjBVNnMM0qxHq47RheVVwsSQrgBA9ZSGV9kdaOfZDPXcHzhG7lBxhj6A7KvfLJBd6Q==} + /electron-to-chromium@1.4.739: + resolution: {integrity: sha512-koRkawXOuN9w/ymhTNxGfB8ta4MRKVW0nzifU17G1UwTWlBg0vv7xnz4nxDnRFSBe9nXMGRgICcAzqXc0PmLeA==} dev: true /emoji-regex@8.0.0: @@ -966,6 +970,7 @@ packages: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 + dev: false /fromentries@1.3.2: resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} @@ -1014,16 +1019,16 @@ packages: is-glob: 4.0.3 dev: true - /glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + /glob@10.3.12: + resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true dependencies: foreground-child: 3.1.1 jackspeak: 2.3.6 - minimatch: 9.0.3 + minimatch: 9.0.4 minipass: 7.0.4 - path-scurry: 1.10.1 + path-scurry: 1.10.2 dev: true /glob@7.2.3: @@ -1122,7 +1127,7 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} dependencies: - binary-extensions: 2.2.0 + binary-extensions: 2.3.0 dev: true /is-extglob@2.1.1: @@ -1191,7 +1196,7 @@ packages: resolution: {integrity: sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==} engines: {node: '>=8'} dependencies: - '@babel/core': 7.23.9 + '@babel/core': 7.24.4 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -1318,8 +1323,8 @@ packages: engines: {node: '>=12.0.0'} hasBin: true dependencies: - '@babel/parser': 7.23.9 - '@jsdoc/salty': 0.2.7 + '@babel/parser': 7.24.4 + '@jsdoc/salty': 0.2.8 '@types/markdown-it': 12.2.3 bluebird: 3.7.2 catharsis: 0.9.0 @@ -1488,12 +1493,14 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + dev: false /mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 + dev: false /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1508,8 +1515,8 @@ packages: brace-expansion: 2.0.1 dev: true - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 @@ -1541,8 +1548,8 @@ packages: hasBin: true dev: true - /mocha@10.3.0: - resolution: {integrity: sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==} + /mocha@10.4.0: + resolution: {integrity: sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==} engines: {node: '>= 14.0.0'} hasBin: true dependencies: @@ -1712,8 +1719,8 @@ packages: engines: {node: '>=8'} dev: true - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + /path-scurry@1.10.2: + resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} engines: {node: '>=16 || 14 >=14.17'} dependencies: lru-cache: 10.2.0 @@ -2102,8 +2109,8 @@ packages: is-typedarray: 1.0.0 dev: true - /typescript@5.3.3: - resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + /typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true dev: true diff --git a/src/config.php b/src/config.php deleted file mode 100644 index 1876a0a..0000000 --- a/src/config.php +++ /dev/null @@ -1,33 +0,0 @@ -fileName = $dbName; - $tmp_db->autoKey = $autoKey; - - if($dbName == 'paths' or $dbName == 'contributions') { - $tmp_db->autoIncrement = false; - } - - $database_list[$dbName] = $tmp_db; -} - -$log_path = "firestorm.log"; - -?> \ No newline at end of file diff --git a/src/index.js b/src/index.js index 9c63f45..5dd9894 100644 --- a/src/index.js +++ b/src/index.js @@ -1,17 +1,20 @@ +const IS_NODE = typeof process === "object"; + try { - if (typeof process === "object") var axios = require("axios").default; -} catch (_error) {} + // ambient axios context in browser + if (IS_NODE) var axios = require("axios").default; +} catch {} /** * @typedef {Object} SearchOption * @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 {"!=" | "==" | ">=" | "<=" | "<" | ">" | "in" | "includes" | "startsWith" | "endsWith" | "array-contains" | "array-contains-none" | "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 + * @typedef {Object} EditFieldOption * @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 @@ -19,7 +22,7 @@ try { */ /** - * @typedef {Object} ValueObject + * @typedef {Object} ValueOption * @property {string} field - Field to search * @property {boolean} [flatten] - Flatten array fields? (default false) */ @@ -30,7 +33,7 @@ try { */ /** - * @typedef {WriteConfirmation} + * @typedef {Object} WriteConfirmation * @property {string} message - Write status */ @@ -71,7 +74,7 @@ const writeToken = () => { /** * Auto-extracts data from Axios request * @ignore - * @param {Promise} request The Axios concerned request + * @param {AxiosPromise} request - Axios request promise */ const __extract_data = (request) => { if (!(request instanceof Promise)) request = Promise.resolve(request); @@ -82,7 +85,7 @@ const __extract_data = (request) => { }; /** - * Class representing a collection + * Represents a Firestorm Collection * @template T */ class Collection { @@ -92,9 +95,9 @@ class Collection { * @param {Function} [addMethods] - Additional methods and data to add to the objects */ constructor(name, addMethods = (el) => el) { - if (name === undefined) throw new SyntaxError("Collection must have a name"); + if (!name) throw new SyntaxError("Collection must have a name"); if (typeof addMethods !== "function") - throw new TypeError("Collection must have an addMethods of type Function"); + throw new TypeError("Collection add methods must be a function"); this.addMethods = addMethods; this.collectionName = name; } @@ -103,106 +106,177 @@ class Collection { * Add user methods to returned data * @private * @ignore - * @param {AxiosPromise} req - Incoming request - * @returns {Object | Object[]} + * @param {any} el - Value to add methods to + * @param {boolean} [nested] - Nest the methods inside an object + * @returns {any} */ - __add_methods(req) { - 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); - }); + __add_methods(el, nested = true) { + // can't add properties on falsy values + if (!el) return el; + if (Array.isArray(el)) return el.map((el) => this.addMethods(el)); + // nested objects + if (nested && typeof el === "object") { + Object.keys(el).forEach((k) => { + el[k] = this.addMethods(el[k]); + }); + return el; + } + + // add directly to single object + return this.addMethods(el); } /** * Auto-extracts data from Axios request * @private * @ignore - * @param {AxiosPromise} request - The Axios concerned request + * @param {AxiosPromise} request - Axios request promise */ __extract_data(request) { return __extract_data(request); } /** - * Send get request and extract data from response + * Send GET request with provided data and return extracted response * @private * @ignore - * @param {Object} data - Body data - * @returns {Promise} data out + * @param {string} command - The read command name + * @param {Object} [data] - Body data + * @param {boolean} [objectLike] - Reject if an object or array isn't being returned + * @returns {Promise} Extracted response */ - __get_request(data) { - const request = - typeof process === "object" - ? axios.get(readAddress(), { - data: data, - }) - : axios.post(readAddress(), data); - return this.__extract_data(request); + __get_request(command, data = {}, objectLike = true) { + const obj = { + collection: this.collectionName, + command: command, + ...data, + }; + const request = IS_NODE + ? axios.get(readAddress(), { data: obj }) + : axios.post(readAddress(), obj); + return this.__extract_data(request).then((res) => { + // reject php error strings if enforcing return type + if (objectLike && typeof res !== "object") return Promise.reject(res); + return res; + }); } /** - * Get an element from the collection - * @param {string | number} id - The ID of the element you want to get - * @returns {Promise} Corresponding value + * Generate POST data with provided data + * @private + * @ignore + * @param {string} command - The write command name + * @param {Object} [value] - The value for the command + * @param {boolean} [multiple] - Used to delete multiple + * @returns {Object} Write data object */ - get(id) { - return this.__get_request({ + __write_data(command, value = undefined, multiple = false) { + const obj = { + token: writeToken(), collection: this.collectionName, - command: "get", - id: id, - }).then((res) => this.__add_methods(res)); + command: command, + }; + + // clone/serialize data if possible (prevents mutating data) + if (value) value = JSON.parse(JSON.stringify(value)); + + if (multiple && Array.isArray(value)) { + value.forEach((v) => { + if (typeof v === "object" && !Array.isArray(v) && v != null) delete v[ID_FIELD_NAME]; + }); + } else if ( + multiple === false && + value !== null && + value !== undefined && + typeof value !== "number" && + typeof value !== "string" && + !Array.isArray(value) + ) { + if (typeof value === "object") value = { ...value }; + delete value[ID_FIELD_NAME]; + } + + if (value) { + if (multiple) obj.values = value; + else obj.value = value; + } + + return obj; } /** - * Get the sha1 hash of the file - * - Can be used to see if same file content without downloading the file + * Get the sha1 hash of the JSON + * - Can be used to compare file content without downloading the file * @returns {string} The sha1 hash of the file */ sha1() { - return this.__get_request({ - collection: this.collectionName, - command: "sha1", + // string value is correct so we don't need validation + return this.__get_request("sha1", {}, false); + } + + /** + * Get an element from the collection by its key + * @param {string | number} key - Key to search + * @returns {Promise} The found element + */ + get(key) { + return this.__get_request("get", { + id: key, + }).then((res) => { + const firstKey = Object.keys(res)[0]; + res[firstKey][ID_FIELD_NAME] = firstKey; + res = res[firstKey]; + return this.__add_methods(res, false); + }); + } + + /** + * Get multiple elements from the collection by their keys + * @param {string[] | number[]} keys - Array of keys to search + * @returns {Promise} The found elements + */ + searchKeys(keys) { + if (!Array.isArray(keys)) return Promise.reject(new TypeError("Incorrect keys")); + + return this.__get_request("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); }); } /** - * Search through collection - * @param {SearchOption[]} searchOptions - Array of search options + * Search through the collection + * @param {SearchOption[]} options - Array of search options * @param {boolean | number} [random] - Random result seed, disabled by default, but can activated with true or a given seed * @returns {Promise} The found elements */ - search(searchOptions, random = false) { - if (!Array.isArray(searchOptions)) - return Promise.reject(new Error("searchOptions shall be an array")); - - searchOptions.forEach((searchOption) => { - if ( - searchOption.field === undefined || - searchOption.criteria === undefined || - searchOption.value === undefined - ) - return Promise.reject(new Error("Missing fields in searchOptions array")); - - if (typeof searchOption.field !== "string") + search(options, random = false) { + if (!Array.isArray(options)) + return Promise.reject(new TypeError("searchOptions shall be an array")); + + options.forEach((option) => { + if (option.field === undefined || option.criteria === undefined || option.value === undefined) + return Promise.reject(new TypeError("Missing fields in searchOptions array")); + + if (typeof option.field !== "string") return Promise.reject( - new Error(`${JSON.stringify(searchOption)} search option field is not a string`), + new TypeError(`${JSON.stringify(option)} search option field is not a string`), ); - if (searchOption.criteria == "in" && !Array.isArray(searchOption.value)) - return Promise.reject(new Error("in takes an array of values")); + if (option.criteria == "in" && !Array.isArray(option.value)) + return Promise.reject(new TypeError("in takes an array of values")); - //TODO: add more strict value field warnings in JS and PHP + // TODO: add more strict value field warnings in JS and PHP }); const params = { - collection: this.collectionName, - command: "search", - search: searchOptions, + search: options, }; if (random !== false) { @@ -212,35 +286,13 @@ class Collection { const seed = parseInt(random); if (isNaN(seed)) return Promise.reject( - new Error("random takes as parameter true, false or an integer value"), + new TypeError("random takes as parameter true, false or an integer value"), ); params.random = { seed }; } } - 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); - }); - } - - /** - * Search specific keys through collection - * @param {string[] | number[]} keys - Array of keys to search - * @returns {Promise} The found elements - */ - searchKeys(keys) { - if (!Array.isArray(keys)) return Promise.reject("Incorrect keys"); - - return this.__get_request({ - collection: this.collectionName, - command: "searchKeys", - search: keys, - }).then((res) => { + return this.__get_request("search", params).then((res) => { const arr = Object.entries(res).map(([id, value]) => { value[ID_FIELD_NAME] = id; return value; @@ -251,26 +303,26 @@ class Collection { } /** - * Returns the whole content of the JSON + * Read the entire collection + * @param {boolean} [original] - Disable ID field injection for easier iteration (default false) * @returns {Promise>} The entire collection */ - readRaw() { - return this.__get_request({ - collection: this.collectionName, - command: "read_raw", - }).then((data) => { + readRaw(original = false) { + return this.__get_request("read_raw").then((data) => { + if (original) return this.__add_methods(data); + // preserve as object Object.keys(data).forEach((key) => { data[key][ID_FIELD_NAME] = key; - this.addMethods(data[key]); }); - return data; + return this.__add_methods(data); }); } /** - * Returns the whole content of the JSON + * Read the entire collection + * - ID values are injected for easier iteration, so this may be different from {@link sha1} * @deprecated Use {@link readRaw} instead * @returns {Promise>} The entire collection */ @@ -280,41 +332,36 @@ class Collection { /** * Get only selected fields from the collection - * - Essentially an upgraded version of readRaw - * @param {SelectOption} selectOption - Select options + * - Essentially an upgraded version of {@link readRaw} + * @param {SelectOption} option - The fields you want to select * @returns {Promise>>} Selected fields */ - select(selectOption) { - if (!selectOption) selectOption = {}; - return this.__get_request({ - collection: this.collectionName, - command: "select", - select: selectOption, + select(option) { + if (!option) option = {}; + return this.__get_request("select", { + select: option, }).then((data) => { Object.keys(data).forEach((key) => { data[key][ID_FIELD_NAME] = key; - this.addMethods(data[key]); }); - - return data; + return this.__add_methods(data); }); } /** * Get all distinct non-null values for a given key across a collection - * @param {ValueObject} valueOption - Value options + * @param {ValueOption} option - 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, + values(option) { + if (!option) return Promise.reject(new TypeError("Value option must be provided")); + if (typeof option.field !== "string") + return Promise.reject(new TypeError("Field must be a string")); + if (option.flatten !== undefined && typeof option.flatten !== "boolean") + return Promise.reject(new TypeError("Flatten must be a boolean")); + + return this.__get_request("values", { + values: option, }).then((data) => // no ID_FIELD or method injection since no ids are returned Object.values(data).filter((d) => d !== null), @@ -322,7 +369,7 @@ class Collection { } /** - * Returns random max entries offsets with a given seed + * Read random elements of the collection * @param {number} max - The maximum number of entries * @param {number} seed - The seed to use * @param {number} offset - The offset to use @@ -332,95 +379,53 @@ class Collection { const params = {}; if (max !== undefined) { if (typeof max !== "number" || !Number.isInteger(max) || max < -1) - return Promise.reject(new Error("Expected integer >= -1 for the max")); + return Promise.reject(new TypeError("Expected integer >= -1 for the max")); params.max = max; } const hasSeed = seed !== undefined; const hasOffset = offset !== undefined; if (hasOffset && !hasSeed) - return Promise.reject(new Error("You can't put an offset without a seed")); + return Promise.reject(new TypeError("You can't put an offset without a seed")); if (hasOffset && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) - return Promise.reject(new Error("Expected integer >= -1 for the max")); + return Promise.reject(new TypeError("Expected integer >= -1 for the max")); if (hasSeed) { if (typeof seed !== "number" || !Number.isInteger(seed)) - return Promise.reject(new Error("Expected integer for the seed")); + return Promise.reject(new TypeError("Expected integer for the seed")); if (!hasOffset) offset = 0; params.seed = seed; params.offset = offset; } - return this.__get_request({ - collection: this.collectionName, - command: "random", + return this.__get_request("random", { random: params, }).then((data) => { Object.keys(data).forEach((key) => { data[key][ID_FIELD_NAME] = key; - this.addMethods(data[key]); }); - return data; + return this.__add_methods(data); }); } /** - * Creates write requests with given value - * @private - * @ignore - * @param {string} command The write command you want - * @param {Object} [value] The value for this command - * @param {boolean | undefined} multiple if I need to delete multiple - * @returns {Object} Write data object - */ - __write_data(command, value = undefined, multiple = false) { - const obj = { - token: writeToken(), - collection: this.collectionName, - command: command, - }; - if (multiple === true && Array.isArray(value)) { - // solves errors with undefined and null values - value.forEach((v) => { - if (typeof value != "number" && typeof value != "string" && !Array.isArray(value)) - delete v[ID_FIELD_NAME]; - }); - } else if ( - multiple === false && - value != null && - value != undefined && - typeof value != "number" && - typeof value != "string" && - !Array.isArray(value) - ) { - // solves errors with undefined and null values - delete value[ID_FIELD_NAME]; - } - if (value) { - if (multiple) obj["values"] = value; - else obj["value"] = value; - } - - return obj; - } - - /** - * Set the entire JSON file contents + * Set the entire content of the collection. + * - Only use this method if you know what you are doing! * @param {Record} value - The value to write * @returns {Promise} Write confirmation */ writeRaw(value) { - if (value === undefined || value === null) { - return Promise.reject(new Error("writeRaw value must not be undefined or null")); - } + if (value === undefined || value === null) + return Promise.reject(new TypeError("writeRaw value must not be undefined or null")); return this.__extract_data(axios.post(writeAddress(), this.__write_data("write_raw", value))); } /** - * Set the entire JSON file contents + * Set the entire content of the collection. + * - Only use this method if you know what you are doing! * @deprecated Use {@link writeRaw} instead * @param {Record} value - The value to write * @returns {Promise} Write confirmation @@ -430,9 +435,10 @@ class Collection { } /** - * Automatically add a value to the JSON file + * Append a value to the collection + * - Only works if autoKey is enabled server-side * @param {T} value - The value (without methods) to add - * @returns {Promise} The generated ID of the added element + * @returns {Promise} The generated key of the added element */ add(value) { return axios @@ -440,15 +446,16 @@ class Collection { .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 Promise.reject(res); return res.id; }); } /** - * Automatically add multiple values to the JSON file - * @param {Object[]} values - The values (without methods) to add - * @returns {Promise} The generated IDs of the added elements + * Append multiple values to the collection + * - Only works if autoKey is enabled server-side + * @param {T[]} values - The values (without methods) to add + * @returns {Promise} The generated keys of the added elements */ addBulk(values) { return this.__extract_data( @@ -457,7 +464,7 @@ class Collection { } /** - * Remove an element from the collection by its ID + * Remove an element from the collection by its key * @param {string | number} key The key from the entry to remove * @returns {Promise} Write confirmation */ @@ -466,7 +473,7 @@ class Collection { } /** - * Remove multiple elements from the collection by their IDs + * Remove multiple elements from the collection by their keys * @param {string[] | number[]} keys The key from the entries to remove * @returns {Promise} Write confirmation */ @@ -475,8 +482,8 @@ class Collection { } /** - * Set a value in the collection by ID - * @param {string} key - The ID of the element you want to edit + * Set a value in the collection by key + * @param {string} key - The key of the element you want to edit * @param {T} value - The value (without methods) you want to edit * @returns {Promise} Write confirmation */ @@ -487,8 +494,8 @@ class Collection { } /** - * Set multiple values in the collection by their IDs - * @param {string[]} keys - The IDs of the elements you want to edit + * Set multiple values in the collection by their keys + * @param {string[]} keys - The keys of the elements you want to edit * @param {T[]} values - The values (without methods) you want to edit * @returns {Promise} Write confirmation */ @@ -499,22 +506,22 @@ class Collection { } /** - * Edit one field of the collection - * @param {EditObject} obj - The edit object - * @returns {Promise<{ success: boolean }>} Edit confirmation + * Edit an element's field in the collection + * @param {EditFieldOption} option - The edit object + * @returns {Promise} Edit confirmation */ - editField(obj) { - const data = this.__write_data("editField", obj, null); + editField(option) { + const data = this.__write_data("editField", option, null); return this.__extract_data(axios.post(writeAddress(), data)); } /** - * Changes one field from an element in this collection - * @param {EditObject[]} objArray The edit object array with operations - * @returns {Promise<{ success: boolean[] }>} Edit confirmation + * Edit multiple elements' fields in the collection + * @param {EditFieldOption[]} options - The edit objects + * @returns {Promise} Edit confirmation */ - editFieldBulk(objArray) { - const data = this.__write_data("editFieldBulk", objArray, undefined); + editFieldBulk(options) { + const data = this.__write_data("editFieldBulk", options, undefined); return this.__extract_data(axios.post(writeAddress(), data)); } } @@ -524,7 +531,7 @@ class Collection { */ const firestorm = { /** - * Change the current Firestorm address + * Change or get the current Firestorm address * @param {string} [newValue] - The new Firestorm address * @returns {string} The stored Firestorm address */ @@ -537,7 +544,7 @@ const firestorm = { }, /** - * Change the current Firestorm token + * Change or get the current Firestorm token * @param {string} [newValue] - The new Firestorm write token * @returns {string} The stored Firestorm write token */ @@ -553,7 +560,7 @@ const firestorm = { * @template T * @param {string} name - The name of the collection * @param {Function} [addMethods] - Additional methods and data to add to the objects - * @returns {Collection} The collection + * @returns {Collection} The collection instance */ collection(name, addMethods = (el) => el) { return new Collection(name, addMethods); @@ -561,15 +568,16 @@ const firestorm = { /** * Create a temporary Firestorm collection with no methods + * @deprecated Use {@link collection} with no second argument instead * @template T * @param {string} name - The table name to get - * @returns {Collection} The collection + * @returns {Collection} The table instance */ table(name) { return this.collection(name); }, - /** Value for the id field when researching content */ + /** Value for the ID field when searching content */ ID_FIELD: ID_FIELD_NAME, /** @@ -582,7 +590,7 @@ const firestorm = { /** * Get a file by its path * @memberof firestorm.files - * @param {string} path - The file path wanted + * @param {string} path - The wanted file path * @returns {Promise} File contents */ get(path) { @@ -598,7 +606,7 @@ const firestorm = { /** * Upload a file * @memberof firestorm.files - * @param {FormData} form - The form data with path, filename, and file + * @param {FormData} form - Form data with path, filename, and file * @returns {Promise} Write confirmation */ upload(form) { @@ -613,7 +621,7 @@ const firestorm = { }, /** - * Deletes a file by path + * Delete a file by its path * @memberof firestorm.files * @param {string} path - The file path to delete * @returns {Promise} Write confirmation @@ -631,8 +639,7 @@ const firestorm = { }, }; +// browser check try { - if (typeof process === "object") module.exports = firestorm; -} catch (_error) { - // normal browser -} + if (IS_NODE) module.exports = firestorm; +} catch {} diff --git a/src/php/classes/FileAccess.php b/src/php/classes/FileAccess.php deleted file mode 100644 index a23f98b..0000000 --- a/src/php/classes/FileAccess.php +++ /dev/null @@ -1,61 +0,0 @@ - $filepath, 'content' => ''); - // open file as binary - $file = fopen($filepath, 'rb'); - - // exit if couldn't find file - if ($file === false) { - if($default == null) - throw new Exception('Could not open file: ' . $filepath); - - // set default value - $fileobj['content'] = $default; - } - - // if no file, puts default value inside - if($file === false) { - file_put_contents($fileobj['filepath'], $fileobj['content'], LOCK_EX); - $file = fopen($filepath, 'rb'); - } - - $fileobj['fd'] = $file; - - // if want the lock, we wait for the shared lock - if($waitLock) { - $lock = flock($file, LOCK_SH); - if (!$lock) { - fclose($file); - throw new Exception('Failed to lock file'); - } - } - - // read file content - $string = ''; - while (!feof($file)) { - $string .= fread($file, 8192); - } - - $fileobj['content'] = $string; - - // if no wait you can close the file - if(!$waitLock) - fclose($file); - - return $fileobj; - } - public static function write($fileobj) { - // unlock and close - flock($fileobj['fd'], LOCK_UN); - fclose($fileobj['fd']); - - if(!is_writable($fileobj['filepath'])) { - throw new HTTPException("PHP script can't write to file, check permission, group and owner.", 400); - } - - $ret = file_put_contents($fileobj['filepath'], $fileobj['content'], LOCK_EX); - return $ret; - } -} \ No newline at end of file diff --git a/src/php/classes/HTTPException.php b/src/php/classes/HTTPException.php deleted file mode 100644 index fe58556..0000000 --- a/src/php/classes/HTTPException.php +++ /dev/null @@ -1,24 +0,0 @@ -code}]: {$this->message}\n"; - } -} \ No newline at end of file diff --git a/src/php/classes/JSONDatabase.php b/src/php/classes/JSONDatabase.php deleted file mode 100644 index 22482e2..0000000 --- a/src/php/classes/JSONDatabase.php +++ /dev/null @@ -1,710 +0,0 @@ -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); - return 400; - } - - // value must not be a sequential array with values inside [1, 2, 3] - // we accept sequential arrays but with objects not primitives - if(is_array($content) and !array_assoc($content)) { - foreach($content as $item) { - $item_type = gettype($item); - if(in_array($item_type, $incorrect_types)) { - throw new HTTPException('write_raw item must not be a ' . $item_type, 400); - return 400; - } - } - } - - // now we know we have an associative array - - // content must be objects - foreach ($content as $key => $item) { - // item must not be primitive - - // we don't accept primitive keys as value - $item_type = gettype($item); - if(in_array($item_type, $incorrect_types)) { - 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); - } - - 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); - } - - $key_var_type = gettype($key); - if($key_var_type != 'string' and $key_var_type != 'integer') - 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') - 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"); - sort($int_keys); - $last_key = count($int_keys) > 0 ? $int_keys[count($int_keys) - 1] + 1 : 0; - } else { - $last_key = uniqid(); - while(array_key_exists($last_key, $arr)) { - $last_key = uniqid(); - } - } - - return strval($last_key); - } - - public function add($value) { - // restricts types to objects only - $value_type = gettype($value); - if($value_type == 'NULL' or $value_type == 'boolean' or $value_type == 'integer' or $value_type == 'double' or $value_type == 'string' or (is_array($value) and count($value) and !array_assoc($value))) - throw new HTTPException('add value must be an object not a ' . $value_type, 400); - - 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); - - // restricts types to non base variables - $value_type = gettype($values); - if($value_type == 'NULL' or $value_type == 'boolean' or $value_type == 'integer' or $value_type == 'double' or $value_type == 'string' or (is_array($values) and count($values) and array_assoc($values))) - throw new HTTPException('value must be an array not a ' . $value_type, 400); - - // so here we have a sequential array type - // now the values inside this array must not be base values - foreach($values as $value) { - $value_type = gettype($value); - if($value_type == 'NULL' or $value_type == 'boolean' or $value_type == 'integer' or $value_type == 'double' or $value_type == 'string' or (is_array($value) and count($value) and !array_assoc($value))) - throw new HTTPException('array value must be an object not a ' . $value_type, 400); - } - - 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); - - $obj = $this->read(true); - 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); - - for($i = 0; $i < count($keys); $i++) { - $key_var_type = gettype($keys[$i]); - if($key_var_type != 'string' and $key_var_type != 'double' and $key_var_type != 'integer') - throw new HTTPException('Incorrect key type', 400); - else - $keys[$i] = strval($keys[$i]); - } - - $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) { - case '!=': - $add = $concernedField != $value; - break; - case '==': - $add = $concernedField == $value; - break; - default: - $add = false; - break; - } - break; - case 'integer': - case 'double': - switch($criteria) { - case '!=': - $add = $concernedField != $value; - break; - case '==': - $add = $concernedField == $value; - break; - case '>=': - $add = $concernedField >= $value; - break; - case '<=': - $add = $concernedField <= $value; - break; - case '<': - $add = $concernedField < $value; - break; - case '>': - $add = $concernedField > $value; - break; - case 'in': - $add = in_array($concernedField, $value); - break; - default: - $add = false; - break; - } - break; - case 'string': - $ignoreCase = array_key_exists('ignoreCase', $condition) && !!$condition['ignoreCase']; - switch($criteria) { - case '!=': - $add = ($ignoreCase ? strcasecmp($concernedField, $value) : strcmp($concernedField, $value)) != 0; - break; - case '==': - $add = ($ignoreCase ? strcasecmp($concernedField, $value) : strcmp($concernedField, $value)) == 0; - break; - case '>=': - $add = ($ignoreCase ? strcasecmp($concernedField, $value) : strcmp($concernedField, $value)) >= 0; - break; - case '<=': - $add = ($ignoreCase ? strcasecmp($concernedField, $value) : strcmp($concernedField, $value)) <= 0; - break; - case '<': - $add = ($ignoreCase ? strcasecmp($concernedField, $value) : strcmp($concernedField, $value)) < 0; - break; - case '>': - $add = ($ignoreCase ? strcasecmp($concernedField, $value) : strcmp($concernedField, $value)) > 0; - break; - case 'includes': - case 'contains': - $add = $value != "" ? (($ignoreCase ? stripos($concernedField, $value) : strpos($concernedField, $value)) !== false) : true; - break; - case 'startsWith': - $add = $value != "" ? (($ignoreCase ? stripos($concernedField, $value) : strpos($concernedField, $value)) === 0) : true; - break; - case 'endsWith': - $end = substr($concernedField, -strlen($value)); - $add = $value != "" ? (($ignoreCase ? strcasecmp($end, $value) : strcmp($end, $value)) === 0) : true; - break; - case 'in': - $notfound = true; - $a_i = 0; - while($a_i < count($value) && $notfound) { - $notfound = ($ignoreCase ? strcasecmp($concernedField, $value[$a_i]) : strcmp($concernedField, $value[$a_i])) != 0; - $a_i++; - } - $add = !$notfound; - break; - default: - $add = false; - break; - } - break; - case 'array': - switch($criteria) { - case "array-contains": - $add = array_contains($concernedField, $value, $ignoreCase); - break; - case "array-contains-any": - $add = array_contains_any($concernedField, $value, $ignoreCase); - break; - case "array-length": - case "array-length-eq": - $add = count($concernedField) == $value; - break; - case "array-length-df": - $add = count($concernedField) != $value; - break; - case "array-length-gt": - $add = count($concernedField) > $value; - break; - case "array-length-lt": - $add = count($concernedField) < $value; - break; - case "array-length-ge": - $add = count($concernedField) >= $value; - break; - case "array-length-le": - $add = count($concernedField) <= $value; - break; - default: - $add = false; - break; - } - default: - break; - } - } else { - $add = false; - } - - $condition_index++; - $el = $el_root; - } - - if($add) { - $res[$key] = $el_root; - } - } - - if($random !== false) { - $seed = false; - if(is_array($random) && array_key_exists('seed', $random)) { - $rawSeed = sec($random['seed']); - if(!is_int($rawSeed)) throw new HTTPException("Seed not an integer value for random search result"); - $seed = intval($rawSeed); - } - $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 - if(in_array($operation, ['set', 'append', 'array-push', 'array-delete', 'array-slice']) and !isset($editObj['value'])) - 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; - return true; - case 'remove': - unset($obj['content'][$id][$field]); - return true; - case 'append': - // 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': - case 'decrement': - // 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 - $change *= $editObj['value']; - } else { - // incorrect value provided, no operation done - 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; - } - - 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; - } - - 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 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'); - - $fields = $selectObj['fields']; - $i = 0; $fields_count = count($fields); - while($i < $fields_count && $verif_fields) { - $verif_fields = gettype($fields[$i]) === 'string'; - ++$i; - } - if(!$verif_fields) throw new HTTPException('fields field incorrect, expected an array of string'); - - $obj = $this->read(); - - $json = $obj['content']; - $result = array(); - foreach ($json as $key => $value) { - $result[$key] = array(); - foreach ($fields as $field) { - if(array_key_exists($field, $value)) $result[$key][$field] = $value[$field]; - } - } - - 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); - } -} - -?> \ No newline at end of file diff --git a/src/php/classes/read/random.php b/src/php/classes/read/random.php deleted file mode 100644 index 54b0756..0000000 --- a/src/php/classes/read/random.php +++ /dev/null @@ -1,83 +0,0 @@ -= -1 for the max'); - - $hasSeed = array_key_exists('seed', $params); - $hasOffset = array_key_exists('offset', $params); - - // offset is relevant only if you get the key - if($hasOffset && !$hasSeed) throw new HTTPException('You can\'t put an offset without a seed'); - - // offset verif - $offset = $hasOffset ? $params['offset'] : 0; - if($hasOffset && (gettype($offset) !== 'integer' || $offset < 0)) throw new HTTPException('Expected integer >= 0 for the offset'); - - // seed verif - $seed = $hasSeed ? $params['seed'] : false; - if($hasSeed && gettype($seed) !== 'integer') throw new HTTPException('Expected integer for the seed'); - - $json = $class->read()['content']; - - return chooseRandom($json, $seed, $max, $offset); -} - -function chooseRandom($json, $seed = false, $max = -1, $offset = 0) { - $keys = array_keys($json); - $keys_selected = array(); - $keys_length = count($keys); - - if($offset >= $keys_length) return array(); // return an empty array, there is no more elements you can get - - if($max == -1 || $max > $keys_length) $max = $keys_length; - - // set random seed just before starting picking - if($seed !== false) mt_srand($seed); - - // the thing is that I need to splice keys from before the offset - for($i = 0; $i < $offset; ++$i) { - $index = mt_rand(0, $keys_length - 1); - array_splice($keys, $index, 1); - - // update keys length - $keys_length = count($keys); - } - - // then while I can get new entries - // -> I still have keys - // -> I am not at maximum - $i = 0; - while($keys_length > 0 && $i < $max) { - // get an index - $index = mt_rand(0, $keys_length - 1); - - // move element to keys selected - $keys_selected = array_merge($keys_selected, array_splice($keys, $index, 1)); - - // recompute keys left - $keys_length = count($keys); - - // next thing - ++$i; - } - - // get objects from keys selected - $result = array(); - for($i = 0; $i < count($keys_selected); ++$i) { - $key = strval($keys_selected[$i]); - $result[$key] = $json[$key]; - } - - return $result; -} \ No newline at end of file diff --git a/src/php/files.php b/src/php/files.php deleted file mode 100644 index d5342a3..0000000 --- a/src/php/files.php +++ /dev/null @@ -1,208 +0,0 @@ -'; - var_dump($val); - echo ''; -} - -function check($var) { - if(isset($var) and !empty($var)) { - return true; - } - return false; -} - -function sec($var) { - return htmlspecialchars($var); -} - -function http_response($body, $code=200) { - header('Content-Type: application/json'); - http_response_code($code); - - echo $body; - - exit(); -} - -function http_json_response($json, $code = 200) { - http_response(json_encode($json), $code); -} - -function http_message($message, $key = 'message', $code = 200) { - $arr = array($key => $message); - http_json_response($arr, $code); -} - -function http_error($code, $message) { - http_message($message, 'error', $code); -} - -function http_success($message) { - http_message($message, 'message', 200); -} - -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) { - if (array() === $arr) return false; - return array_keys($arr) !== range(0, count($arr) - 1); -} - -function array_sequential(array $arr) { - return !array_assoc($arr); -} - -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; -} - -function cors() { -// Allow from any origin -if (isset($_SERVER['HTTP_ORIGIN'])) { - header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}"); - header('Access-Control-Allow-Credentials: true'); - header('Access-Control-Max-Age: 86400'); // cache for 1 day -} - -// Access-Control headers are received during OPTIONS requests -if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { - - if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) - 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']}"); - - exit(0); -} -} - -function removeDots($path) { - $root = ($path[0] === '/') ? '/' : ''; - - $segments = explode('/', trim($path, '/')); - $ret = array(); - foreach($segments as $segment){ - if (($segment == '.') || strlen($segment) === 0) { - continue; - } - if ($segment == '..') { - array_pop($ret); - } else { - array_push($ret, $segment); - } - } - return $root . implode('/', $ret); -} - -if (! function_exists('str_ends_with')) { - function str_ends_with(string $haystack, string $needle): bool - { - $needle_len = strlen($needle); - return ($needle_len === 0 || 0 === substr_compare($haystack, $needle, - $needle_len)); - } -} - -?> diff --git a/src/tokens.php b/src/tokens.php deleted file mode 100644 index 3d8b3a4..0000000 --- a/src/tokens.php +++ /dev/null @@ -1,7 +0,0 @@ - "KGKbkjJKKLMhnkJjkbjkk" -); - -$db_tokens = array_values($db_tokens_map); \ No newline at end of file diff --git a/tests/config.php b/tests/config.php index 630cc08..525650b 100644 --- a/tests/config.php +++ b/tests/config.php @@ -1,12 +1,7 @@ 2 and $prop[2] == true; - - $tmp_db = new JSONDatabase; - $tmp_db->fileName = $dbName; - $tmp_db->autoKey = $autoKey; - $tmp_db->autoIncrement = $autoKeyIncrement; - - $database_list[$dbName] = $tmp_db; -} +$database_list = array( + // test with constructor/optional args + "house" => new JSONDatabase('house', false) +); + +// test without constructor +$tmp = new JSONDatabase; +$tmp->fileName = 'base'; +$tmp->autoKey = true; +$tmp->autoIncrement = true; + +$database_list[$tmp->fileName] = $tmp; -$log_path = "firestorm.log"; -?> \ No newline at end of file +$log_path = 'firestorm.log'; diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 8bc9f58..d6adc87 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -1,6 +1,5 @@ -const chai = require("chai"); +const { expect } = require("chai"); const FormData = require("form-data"); -const { expect } = chai; const firestorm = require(".."); @@ -57,8 +56,8 @@ const resetDatabaseContent = async () => { await base.writeRaw(content).catch((err) => console.error(err)); houseCollection = firestorm.collection(HOUSE_DATABASE_NAME); - const raw_house = JSON.parse(fs.readFileSync(HOUSE_DATABASE_FILE).toString()); - await houseCollection.writeRaw(raw_house); + const rawHouse = JSON.parse(fs.readFileSync(HOUSE_DATABASE_FILE).toString()); + await houseCollection.writeRaw(rawHouse); }; describe("File upload, download and delete", () => { @@ -117,16 +116,12 @@ describe("File upload, download and delete", () => { const formData = new FormData(); formData.append("path", "/"); readFile(path.join(__dirname, "lyrics.txt")) - .catch(() => { - done(new Error("Should not succeed at first")); - }) + .catch(() => done(new Error("Should not succeed at first"))) .then((res) => { formData.append("file", res, "lyrics.txt"); return firestorm.files.upload(formData); }) - .then((res) => { - done(res); - }) + .then((res) => done(res)) .catch((uploadError) => { expect(uploadError).not.to.be.undefined; expect(uploadError.response).not.to.be.undefined; @@ -150,9 +145,7 @@ describe("File upload, download and delete", () => { const lyricsPromise = readFile(path.join(__dirname, "lyrics.txt")); // get done now - lyricsPromise.catch(() => { - done("File read should not failed"); - }); + lyricsPromise.catch(() => done("File read should not failed")); const uploadPromise = lyricsPromise.then((res) => { // add file to form data @@ -186,8 +179,8 @@ describe("GET operations", () => { before(async () => { base = firestorm.collection(DATABASE_NAME); - const raw_content = fs.readFileSync(DATABASE_FILE).toString(); - content = JSON.parse(raw_content); + const rawContent = fs.readFileSync(DATABASE_FILE).toString(); + content = JSON.parse(rawContent); await resetDatabaseContent(); }); @@ -207,23 +200,38 @@ describe("GET operations", () => { }); }); - it("returns the exact content of the file", (done) => { + it("returns the exact contents of the file", (done) => { + base + .readRaw(true) + .then((res) => { + expect(res).deep.equals(content, "Content different"); + done(); + }) + .catch(done); + }); + + it("injects ID values into every item", (done) => { base .readRaw() .then((res) => { + Object.entries(res).forEach(([k, v]) => + expect(v).to.have.property(firestorm.ID_FIELD, k, "Missing ID field"), + ); Object.keys(res).forEach((key) => delete res[key][firestorm.ID_FIELD]); expect(res).deep.equals(content, "Content different"); done(); }) - .catch((err) => done(err)); + .catch(done); }); + }); + describe("sha1()", () => { it("sha1 content hash is the same", (done) => { base .sha1() .then((res) => { - const file_sha1 = crypto.createHash("sha1").update(JSON.stringify(content)).digest("hex"); - expect(res).equals(file_sha1, "Content hash different"); + const sha1 = crypto.createHash("sha1").update(JSON.stringify(content)).digest("hex"); + expect(res).equals(sha1, "Content hash different"); done(); }) .catch((err) => { @@ -242,7 +250,7 @@ describe("GET operations", () => { expect(res).deep.equals(content[0], "Content different"); done(); }) - .catch((err) => done(err)); + .catch(done); }); it("number parameter should return the correct value", (done) => { @@ -253,7 +261,7 @@ describe("GET operations", () => { expect(res).deep.equals(content[0], "Content different"); done(); }) - .catch((err) => done(err)); + .catch(done); }); it("string and number parameters gives the same result", (done) => { @@ -263,7 +271,7 @@ describe("GET operations", () => { expect(results[0]).deep.equals(results[1], "Content different"); done(); }) - .catch((err) => done(err)); + .catch(done); }); }); @@ -274,10 +282,7 @@ describe("GET operations", () => { .then(() => { done(new Error("Parameter should be an array of string or number")); }) - .catch((err) => { - expect(err).to.equal("Incorrect keys"); - done(); - }); + .catch(() => done()); }); it("returns empty results when no found", (done) => { @@ -285,13 +290,11 @@ describe("GET operations", () => { .searchKeys([5, 7]) .then((res) => { // expected [] - expect(res).to.be.a("array", "Value should be an array"); + expect(res).to.be.an("array", "Value should be an array"); expect(res).to.have.lengthOf(0, "Value should be empty array"); done(); }) - .catch(() => { - done(new Error("Should not reject")); - }); + .catch(() => done(new Error("Should not reject"))); }); it("returns correct content", (done) => { @@ -307,15 +310,13 @@ describe("GET operations", () => { expect(res).deep.equals(expected, "Result content doesn't match"); done(); }) - .catch(() => { - done(new Error("Should not reject")); - }); + .catch(() => done(new Error("Should not reject"))); }); }); describe("search(searchOptions)", () => { - // [criteria, field, value, idsFound] - const test_array = [ + // [criteria, field, value, idsFound, ignoreCase] + const testArray = [ ["!=", "age", 13, ["0", "2"]], ["==", "age", 13, ["1"]], ["==", "age", 25, []], @@ -342,6 +343,9 @@ describe("GET operations", () => { ["array-contains", "qualities", "strong", ["0", "1"]], ["array-contains", "qualities", "sTRoNG", ["0", "1"], true], ["array-contains", "qualities", "handsome", []], + ["array-contains-none", "qualities", ["strong"], ["2"]], + ["array-contains-none", "qualities", ["sTrOnG"], ["2"], true], + ["array-contains-none", "qualities", ["strong", "calm"], []], ["array-contains-any", "qualities", ["intelligent", "calm"], ["0", "2"]], ["array-contains-any", "qualities", ["intELLIGent", "CALm"], ["0", "2"], true], ["array-contains-any", "qualities", ["fast", "flying"], []], @@ -358,32 +362,28 @@ describe("GET operations", () => { ["array-length-ge", "friends", 7, []], ]; - test_array.forEach((test_item) => { - const criteria = test_item[0]; - const field = test_item[1]; - const value = test_item[2]; - const ids_found = test_item[3]; - const ignore_case = !!test_item[4]; - it(`${criteria} criteria${ids_found.length == 0 ? " (empty result)" : ""}${ - ignore_case ? " (case insensitive)" : "" + testArray.forEach(([criteria, field, value, idsFound, ignoreCase]) => { + ignoreCase = !!ignoreCase; + it(`${criteria} criteria${idsFound.length == 0 ? " (empty result)" : ""}${ + ignoreCase ? " (case insensitive)" : "" }`, (done) => { base .search([ { - criteria: criteria, - field: field, - value: value, - ignoreCase: ignore_case, + criteria, + field, + value, + ignoreCase, }, ]) .then((res) => { - expect(res).to.be.a("array", "Search result must be an array"); + expect(res).to.be.an("array", "Search result must be an array"); expect(res).to.have.lengthOf( - ids_found.length, + idsFound.length, "Expected result have not correct length", ); expect(res.map((el) => el[firestorm.ID_FIELD])).to.deep.equal( - ids_found, + idsFound, "Incorrect result search", ); done(); @@ -398,7 +398,7 @@ describe("GET operations", () => { describe("search(searchOptions, random)", () => { describe("Nested keys test", () => { - it("doesn't crash if nested key unknown", (done) => { + it("doesn't crash if unknown nested key", (done) => { base .search([ { @@ -412,9 +412,7 @@ describe("GET operations", () => { expect(res.length).to.equal(0); done(); }) - .catch((err) => { - done(err); - }); + .catch(done); }); it("can find correct nested value", (done) => { base @@ -444,15 +442,14 @@ describe("GET operations", () => { ]); done(); }) - .catch((err) => { - done(err); - }); + .catch((err) => done(err)); }); }); - let incorrect = [null, "gg", ""]; // undefined works because random becomes default parameter false, so false works too - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} seed rejects`, (done) => { + // undefined works because random becomes default parameter false, so false works too + const incorrect = [null, "gg", ""]; + incorrect.forEach((incor) => { + it(`${JSON.stringify(incor)} seed rejects`, (done) => { base .search( [ @@ -462,12 +459,10 @@ describe("GET operations", () => { value: "", }, ], - unco, + incor, ) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); @@ -483,7 +478,7 @@ describe("GET operations", () => { ], true, ) - .then((_) => done()) + .then(() => done()) .catch((err) => { console.error(err); done("Should not reject with error " + JSON.stringify(err)); @@ -523,22 +518,18 @@ describe("GET operations", () => { it("requires a fields field", (done) => { base .select(undefined) - .then((res) => { - done("Did not expect it to success"); - }) + .then(() => done("Did not expect it to success")) .catch(() => done()); }); describe("requires field to be a string array", () => { // all incorrect values must catch - let incorrect = [undefined, null, false, 5, 12.5, "gg", { toto: "tata" }]; - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} value`, (done) => { + const incorrect = [undefined, null, false, 5, 12.5, "gg", { toto: "tata" }]; + incorrect.forEach((incor) => { + it(`${JSON.stringify(incor)} value rejects`, (done) => { base - .select({ fields: unco }) + .select({ fields: incor }) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); }); @@ -548,12 +539,8 @@ describe("GET operations", () => { it(`${JSON.stringify(val)} value`, (done) => { base .select({ fields: val }) - .then(() => { - done(); - }) - .catch((err) => { - done(err); - }); + .then(() => done()) + .catch(done); }); }); }); @@ -561,31 +548,27 @@ describe("GET operations", () => { describe(`must accept only string arrays`, () => { // incorrect arrays incorrect = [undefined, null, false, 5, 12.5, {}]; - incorrect.forEach(async (unco) => { - it(`[${JSON.stringify(unco)}] value`, (done) => { + incorrect.forEach((incor) => { + it(`[${JSON.stringify(incor)}] value rejects`, (done) => { base - .select({ fields: [unco] }) - .then(() => done(`[${JSON.stringify(unco)}] value passed`)) - .catch(() => { - done(); - }); + .select({ fields: [incor] }) + .then(() => done(`[${JSON.stringify(incor)}] value passed`)) + .catch(() => done()); }); }); }); it("Gives correct value", (done) => { - const fields_chosen = ["name", "age"]; - Promise.all([base.readRaw(), base.select({ fields: fields_chosen })]) - .then((results) => { - let raw = results[0]; + const chosenFields = ["name", "age"]; + Promise.all([base.readRaw(), base.select({ fields: chosenFields })]) + .then(([raw, selectResult]) => { Object.keys(raw).forEach((k) => { - Object.keys(raw[k]).forEach((el_k) => { - if (!fields_chosen.includes(el_k) || typeof raw[k][el_k] === "function") { - delete raw[k][el_k]; + Object.keys(raw[k]).forEach((el) => { + if (!chosenFields.includes(el) || typeof raw[k][el] === "function") { + delete raw[k][el]; } }); }); - let selectResult = results[1]; Object.keys(selectResult).forEach((k) => { delete selectResult[k][firestorm.ID_FIELD]; }); @@ -593,7 +576,7 @@ describe("GET operations", () => { expect(selectResult).to.be.deep.equal(raw, `contents are different`); done(); }) - .catch((err) => done(err)); + .catch(done); }); }); @@ -601,75 +584,131 @@ describe("GET operations", () => { it("requires a field", (done) => { base .values() - .then((res) => { - done("Did not expect it to succeed"); - }) + .then(() => done("Did not expect it to succeed")) .catch(() => done()); }); it("doesn't require a flatten field", (done) => { base .values({ field: "name" }) - .then((res) => { - done(); - }) + .then(() => 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"); - }) + .then(() => done("Did not expect it to succeed")) .catch(() => done()); }); + + describe("needs string field value", () => { + const incorrect = [null, false, 5.5, -5, { key: "val" }, ["asdf"]]; + incorrect.forEach((incor) => { + it(`${JSON.stringify(incor)} value rejects`, (done) => { + base + .values({ field: incor }) + .then(() => done("Did not expect it to succeed")) + .catch(() => done()); + }); + }); + }); + + describe("returns the right content", () => { + it("works on primitive without flattening", (done) => { + base + .values({ field: "age" }) + .then((res) => { + // sort values to prevent possible ordering issues + const expected = Array.from(new Set(Object.values(content).map((v) => v.age))); + expect(res.sort()).to.deep.equal(expected.sort()); + done(); + }) + .catch(done); + }); + + it("works with an array with flattening", (done) => { + base + .values({ field: "friends", flatten: true }) + .then((res) => { + const expected = Array.from( + new Set( + Object.values(content) + .map((v) => v.friends) + .flat(), + ), + ); + expect(res.sort()).to.deep.equal(expected.sort()); + done(); + }) + .catch(done); + }); + + it("works on primitive with flattening", (done) => { + base + .values({ field: "age", flatten: true }) + .then((res) => { + // flatten field gets ignored on primitives (easier to handle) + const expected = Array.from(new Set(Object.values(content).map((v) => v.age))); + expect(res.sort()).to.deep.equal(expected.sort()); + done(); + }) + .catch(done); + }); + + it("works on an array without flattening", (done) => { + base + .values({ field: "friends" }) + .then((res) => { + const values = Object.values(content).map((v) => v.friends); + const unique = values.filter( + (el, i) => + i === values.findIndex((obj) => JSON.stringify(obj) === JSON.stringify(el)), + ); + expect(res.sort()).to.deep.equal(unique.sort()); + done(); + }) + .catch(done); + }); + }); }); describe("random(max, seed, offset)", () => { it("doesn't require parameters", (done) => { base .random() - .then((res) => { - done(); - }) + .then(() => done()) .catch(() => done("Did not expect it to fail")); }); it("passes with undefined parameters", (done) => { base .random(undefined, undefined, undefined) - .then((res) => { - done(); - }) + .then(() => done()) .catch(() => done("Did not expect it to fail")); }); describe("requires max parameter to be an integer >= -1", () => { // all incorrect values must catch - let incorrect = [null, false, "gg", 5.5, -5, -2]; // undefined works because max is the whole collection then - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} value`, (done) => { + const incorrect = [null, false, "gg", 5.5, -5, -2]; // undefined works because max is the whole collection then + incorrect.forEach((incor) => { + it(`${JSON.stringify(incor)} value`, (done) => { base - .random(unco) + .random(incor) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); }); describe("requires seed parameter to be an integer", () => { // all incorrect values must catch - let incorrect = [null, false, "gg", 5.5]; // undefined works because then seed is automatic - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} value`, (done) => { + const incorrect = [null, false, "gg", 5.5]; // undefined works because then seed is automatic + incorrect.forEach((incor) => { + it(`${JSON.stringify(incor)} value`, (done) => { base - .random(5, unco) + .random(5, incor) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); }); @@ -683,31 +722,27 @@ describe("GET operations", () => { describe("requires offset parameter to be an integer >= 0", () => { // all incorrect values must catch - let incorrect = [null, false, "gg", 5.5, -1]; // undefined works because then offset is 0 - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} value`, (done) => { + const incorrect = [null, false, "gg", 5.5, -1]; // undefined works because then offset is 0 + incorrect.forEach((incor) => { + it(`${JSON.stringify(incor)} value`, (done) => { base - .random(5, 69, unco) + .random(5, 69, incor) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); }); }); }); -describe("PUT operations", () => { +describe("POST operations", () => { describe("writeRaw operations", () => { it("Rejects when incorrect token", (done) => { firestorm.token("LetsGoToTheMall"); base .writeRaw({}) - .then((res) => { - done(res); - }) + .then((res) => done(res)) .catch((err) => { if ("response" in err && err.response.status == 403) { done(); @@ -715,13 +750,11 @@ describe("PUT operations", () => { } done(new Error("Should return 403")); }) - .finally(() => { - firestorm.token(TOKEN); - }); + .finally(() => firestorm.token(TOKEN)); }); describe("You must give a correct value", () => { - const incorrect_bodies = [ + const incorrectBodies = [ undefined, null, false, @@ -733,13 +766,11 @@ describe("PUT operations", () => { { 5: "is" }, ]; - incorrect_bodies.forEach((body, index) => { + incorrectBodies.forEach((body, index) => { it(`${JSON.stringify(body)} value rejects`, (done) => { base .writeRaw(body) - .then((res) => { - done(new Error(`Should not fulfill returning ${JSON.stringify(res)}`)); - }) + .then((res) => done(new Error(`Should not fulfill returning ${JSON.stringify(res)}`))) .catch((err) => { if (index < 2) { expect(err).to.be.an("error"); @@ -764,9 +795,7 @@ describe("PUT operations", () => { it("but it can write an empty content : {}", (done) => { base .writeRaw({}) - .then(() => { - done(); - }) + .then(() => done()) .catch((err) => { console.trace(err); done(err); @@ -787,9 +816,7 @@ describe("PUT operations", () => { outdoor: true, furniture: ["table", "chair", "flowerpot"], }) - .then(() => { - done(new Error("This request should not fulfill")); - }) + .then(() => done(new Error("This request should not fulfill"))) .catch((err) => { if ("response" in err && err.response.status == 400) { done(); @@ -806,7 +833,7 @@ describe("PUT operations", () => { }); it("must give incremented key when adding on a auto key auto increment", (done) => { - const last_id = Object.keys(content).pop(); + const lastID = Object.keys(content).pop(); base .add({ name: "Elliot Alderson", @@ -816,22 +843,20 @@ describe("PUT operations", () => { }) .then((id) => { expect(id).to.be.a("string"); - expect(id).to.equals(String(parseInt(last_id) + 1)); + expect(id).to.equals(String(parseInt(lastID) + 1)); done(); }) - .catch((err) => { - done(err); - }); + .catch(done); }); describe("It should not accept incorrect values", () => { - const incorrect_values = [undefined, null, false, 16, "Muse", [1, 2, 3]]; + const incorrectValues = [undefined, null, false, 16, "Muse", [1, 2, 3]]; // I wanted to to test [] but serialized it's the same as an empty object which must be accepted - incorrect_values.forEach((unco) => { - it(`${JSON.stringify(unco)} value rejects`, (done) => { + incorrectValues.forEach((incor) => { + it(`${JSON.stringify(incor)} value rejects`, (done) => { base - .add(unco) + .add(incor) .then((res) => { done(new Error(`Should not fulfill with res ${res}`)); }) @@ -847,7 +872,7 @@ describe("PUT operations", () => { }); describe("It should accept correct values", () => { - const correct_values = [ + const correctValues = [ {}, { name: "Elliot Alderson", @@ -857,16 +882,12 @@ describe("PUT operations", () => { }, ]; - correct_values.forEach((co, index) => { + correctValues.forEach((co, index) => { it(`${index === 0 ? "Empty object" : "Complex object"} should fulfill`, (done) => { base .add(co) - .then(() => { - done(); - }) - .catch((err) => { - done(err); - }); + .then(() => done()) + .catch(done); }); }); }); @@ -889,20 +910,15 @@ describe("PUT operations", () => { }); describe("must reject with incorrect base values", () => { - const incorrect_values = [undefined, null, false, 16, "Muse", [1, 2, 3]]; + const incorrectValues = [undefined, null, false, 16, "Muse", [1, 2, 3]]; - incorrect_values.forEach((unco) => { - it(`${JSON.stringify(unco)} value rejects`, (done) => { + incorrectValues.forEach((incor) => { + it(`${JSON.stringify(incor)} value rejects`, (done) => { base - .addBulk(unco) - .then((res) => { - done(new Error(`Should not fulfill with res ${res}`)); - }) + .addBulk(incor) + .then((res) => done(new Error(`Should not fulfill with res ${res}`))) .catch((err) => { - if ("response" in err && err.response.status == 400) { - done(); - return; - } + if ("response" in err && err.response.status == 400) return done(); done(new Error(`Should return 400 not ${JSON.stringify(err)}`)); }); }); @@ -910,12 +926,12 @@ describe("PUT operations", () => { }); describe("must reject with incorrect array", () => { - const incorrect_values = [undefined, null, false, 16, "Muse", [1, 2, 3]]; + const incorrectValues = [undefined, null, false, 16, "Muse", [1, 2, 3]]; - incorrect_values.forEach((unco) => { - it(`[${JSON.stringify(unco)}] value rejects`, (done) => { + incorrectValues.forEach((incor) => { + it(`[${JSON.stringify(incor)}] value rejects`, (done) => { base - .addBulk([unco]) + .addBulk([incor]) .then((res) => { done(new Error(`Should not fulfill with res ${res}`)); }) @@ -939,22 +955,22 @@ describe("PUT operations", () => { base .addBulk([{}]) .then((res) => { - expect(res).to.be.a("array"); + expect(res).to.be.an("array"); expect(res).to.have.length(1); done(); }) .catch((err) => { - console.log(err); + console.error(err); done(err); }); }); it("should accept correct array value", (done) => { - const in_value = [{ a: 1 }, { b: 2 }, { c: 3 }]; + const inValue = [{ a: 1 }, { b: 2 }, { c: 3 }]; base - .addBulk(in_value) + .addBulk(inValue) .then((res) => { - expect(res).to.be.a("array"); + expect(res).to.be.an("array"); expect(res).to.have.length(3); res.forEach((id) => { expect(id).to.be.a("string"); @@ -962,19 +978,18 @@ describe("PUT operations", () => { return Promise.all([res, base.searchKeys(res)]); }) .then((results) => { - const search_results = results[1]; - expect(search_results).to.be.a("array"); - expect(search_results).to.have.length(3); + const searchResults = results[1]; + expect(searchResults).to.be.an("array"); + expect(searchResults).to.have.length(3); - const ids_generated = results[0]; + const idsGenerated = results[0]; // modify results and add ID - in_value.map((el, index) => { - el[firestorm.ID_FIELD] = ids_generated[index]; - + inValue.map((el, index) => { + el[firestorm.ID_FIELD] = idsGenerated[index]; return el; }); - expect(search_results).to.be.deep.equals(in_value); + expect(searchResults).to.be.deep.equals(inValue); done(); }) .catch((err) => { @@ -986,12 +1001,11 @@ describe("PUT operations", () => { }); describe("remove operations", () => { - describe("must accept only string keys", () => { - const incorrect_values = [ + describe("must reject non-keyable values", () => { + const incorrectValues = [ undefined, null, false, - 16, 22.2, [], [1, 2, 3], @@ -999,10 +1013,10 @@ describe("PUT operations", () => { { "i'm": "batman" }, ]; - incorrect_values.forEach((unco) => { - it(`${JSON.stringify(unco)} value rejects`, (done) => { + incorrectValues.forEach((incor) => { + it(`${JSON.stringify(incor)} value rejects`, (done) => { base - .remove(unco) + .remove(incor) .then((res) => { done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) @@ -1049,12 +1063,12 @@ describe("PUT operations", () => { describe("removeBulk operations", () => { describe("must accept only string array", () => { - const incorrect_values = [undefined, null, false, [], [1, 2, 3], {}, { "i'm": "batman" }]; + const incorrectValues = [undefined, null, false, [], [1, 2, 3], {}, { "i'm": "batman" }]; - incorrect_values.forEach((unco) => { - it(`[${JSON.stringify(unco)}] value rejects`, (done) => { + incorrectValues.forEach((incor) => { + it(`[${JSON.stringify(incor)}] value rejects`, (done) => { base - .removeBulk([unco]) + .removeBulk([incor]) .then((res) => { done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) @@ -1114,17 +1128,15 @@ describe("PUT operations", () => { }); describe("0 values fulfill", () => { - const correct_values = ["0", 0, 0.0]; + const correctValues = ["0", 0, 0.0]; - correct_values.forEach((unco) => { - it(`${JSON.stringify(unco)} value fulfills`, (done) => { + correctValues.forEach((incor) => { + it(`${JSON.stringify(incor)} value fulfills`, (done) => { base - .set(unco, tmp) - .then((res) => { - done(); - }) + .set(incor, tmp) + .then(() => done()) .catch((err) => { - if ("response" in err) console.log(err.response.data); + if ("response" in err) console.error(err.response.data); done(new Error(err)); }); }); @@ -1132,12 +1144,12 @@ describe("PUT operations", () => { }); describe("Key must be a string or an integer", () => { - const incorrect_values = [undefined, null, false, [], [1, 2, 3], {}, { "i'm": "batman" }]; + const incorrectValues = [undefined, null, false, [], [1, 2, 3], {}, { "i'm": "batman" }]; - incorrect_values.forEach((unco) => { - it(`${JSON.stringify(unco)} value rejects`, (done) => { + incorrectValues.forEach((incor) => { + it(`${JSON.stringify(incor)} value rejects`, (done) => { base - .set(unco, tmp) + .set(incor, tmp) .then((res) => { done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) @@ -1152,12 +1164,12 @@ describe("PUT operations", () => { }); describe("Value must be an object", () => { - const incorrect_values = [undefined, null, false, 16, 22.2, [1, 2, 3]]; + const incorrectValues = [undefined, null, false, 16, 22.2, [1, 2, 3]]; - incorrect_values.forEach((unco) => { - it(`${JSON.stringify(unco)} value rejects`, (done) => { + incorrectValues.forEach((incor) => { + it(`${JSON.stringify(incor)} value rejects`, (done) => { base - .set("1", unco) + .set("1", incor) .then((res) => { done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) @@ -1242,12 +1254,12 @@ describe("PUT operations", () => { }); describe("keys should be an array of string", () => { - const incorrect_values = [undefined, null, false, [], [1, 2, 3], {}, { "i'm": "batman" }]; + const incorrectValues = [undefined, null, false, [], [1, 2, 3], {}, { "i'm": "batman" }]; - incorrect_values.forEach((unco) => { - it(`[${JSON.stringify(unco)}] value rejects`, (done) => { + incorrectValues.forEach((incor) => { + it(`[${JSON.stringify(incor)}] value rejects`, (done) => { base - .setBulk([unco], [tmp]) + .setBulk([incor], [tmp]) .then((res) => { done(new Error(`Should not fulfill with value ${JSON.stringify(res)}`)); }) @@ -1285,21 +1297,72 @@ describe("PUT operations", () => { await resetDatabaseContent(); }); - describe("Reject with incorrect values", () => { - const incorrect_values = [undefined, null, false, 16, 0.5, "", "gg", [], {}]; + it("Rejects with unknown operation", (done) => { + base + .editField({ + id: "2", + operation: "smile", + field: "name", + }) + .then((res) => done(new Error("Should not fulfill with " + JSON.stringify(res)))) + .catch((err) => { + if ("response" in err && err.response.status == 400) return done(); + done(new Error(`Should return 400 not ${JSON.stringify(err)}`)); + }); + }); - incorrect_values.forEach((unco) => { - it(`'${JSON.stringify(unco)}' value rejects`, (done) => { + describe("Edits the correct values", () => { + // field, operation, value, expected + const testArray = [ + ["name", "set", "new name", "new name"], + ["name", "append", " yes", "new name yes"], + ["name", "remove", null, undefined], + ["amazing", "invert", null, false], + ["age", "increment", null, 46], + ["age", "increment", 3, 49], + ["age", "decrement", null, 48], + ["age", "decrement", 3, 45], + ["friends", "array-push", "Bob", ["Dwight", "Bob"]], + ["qualities", "array-delete", 2, ["pretty", "wild"]], + ]; + + testArray.forEach(([field, operation, value, expected]) => { + it(`'${operation}' on '${field}' yields ${JSON.stringify(expected)}`, (done) => { + const obj = { + id: 2, + field, + operation, + }; + // not required + if (value !== null) obj.value = value; base - .editField(unco) + .editField(obj) + .then((res) => { + expect(res).to.deep.equal( + { message: "Successful editField command" }, + "Should not fail", + ); + return base.get(2); + }) .then((res) => { - done(new Error("Should not fulfill with " + JSON.stringify(res))); + expect(res[field]).to.deep.equal(expected, "Should be the new set values"); + done(); }) + .catch(done); + }); + }); + }); + + describe("Reject with incorrect values", () => { + const incorrectValues = [undefined, null, false, 16, 0.5, "", "gg", [], {}]; + + incorrectValues.forEach((incor) => { + it(`'${JSON.stringify(incor)}' value rejects`, (done) => { + base + .editField(incor) + .then((res) => done(new Error("Should not fulfill with " + JSON.stringify(res)))) .catch((err) => { - if ("response" in err && err.response.status == 400) { - done(); - return; - } + if ("response" in err && err.response.status == 400) return done(); done(new Error(`Should return 400 not ${JSON.stringify(err)}`)); }); }); @@ -1314,7 +1377,7 @@ describe("PUT operations", () => { ]; for (let i = 0; i < args.length; ++i) { - let obj = {}; + const obj = {}; args.slice(0, i + 1).forEach((el) => { obj[el[0]] = el[1]; }); @@ -1322,14 +1385,9 @@ describe("PUT operations", () => { it(`${i + 1} args is not enough`, (done) => { base .editField(obj) - .then((res) => { - done(new Error("Should not fulfill with " + JSON.stringify(res))); - }) + .then((res) => done(new Error("Should not fulfill with " + JSON.stringify(res)))) .catch((err) => { - if ("response" in err && err.response.status == 400) { - done(); - return; - } + if ("response" in err && err.response.status == 400) return done(); done(new Error(`Should return 400 not ${JSON.stringify(err)}`)); }); }); @@ -1337,7 +1395,7 @@ describe("PUT operations", () => { }); describe("Rejects operations with value required", () => { - ["set", "append", "array-push", "array-delete", "array-slice"].forEach((op) => { + ["set", "append", "array-push", "array-delete", "array-splice"].forEach((op) => { it(`${op} operation needs a value`, (done) => { base .editField({ @@ -1345,37 +1403,13 @@ describe("PUT operations", () => { operation: op, field: "name", }) - .then((res) => { - done(new Error("Should not fulfill with " + JSON.stringify(res))); - }) + .then((res) => done(new Error("Should not fulfill with " + JSON.stringify(res)))) .catch((err) => { - if ("response" in err && err.response.status == 400) { - done(); - return; - } + if ("response" in err && err.response.status == 400) return done(); done(new Error(`Should return 400 not ${JSON.stringify(err)}`)); }); }); }); }); - - it("Rejects with unknown operation", (done) => { - base - .editField({ - id: "2", - operation: "smile", - field: "name", - }) - .then((res) => { - done(new Error("Should not fulfill with " + JSON.stringify(res))); - }) - .catch((err) => { - if ("response" in err && err.response.status == 400) { - done(); - return; - } - done(new Error(`Should return 400 not ${JSON.stringify(err)}`)); - }); - }); }); }); diff --git a/tests/php_setup.js b/tests/php_setup.js index a07f7d3..451d89f 100644 --- a/tests/php_setup.js +++ b/tests/php_setup.js @@ -1,11 +1,12 @@ -const fs = require("fs").promises; +const { mkdtemp, mkdir, symlink, unlink } = require("fs").promises; const { existsSync } = require("fs"); -const path = require("path"); -const os = require("os"); +const { join, relative, dirname, basename } = require("path"); +const { tmpdir } = require("os"); const { glob } = require("glob"); const copy = require("recursive-copy"); -const child_process = require("child_process"); +const { execSync, spawn } = require("child_process"); +const PHP_PATH = "php"; const PHP_SERVER_START_DELAY = 2000; const PORT = 8000; @@ -14,117 +15,82 @@ console.log("Creating tmp folder..."); async function setup_php() { // create tmp folder for PHP - let tmpFolder; - child_process.execSync("rm -rf /tmp/php-*"); - - await fs - .mkdtemp(path.join(os.tmpdir(), "php-")) - .then((folder) => { - tmpFolder = folder; - console.log(`Created ${tmpFolder}`); - - console.log( - "Moving PHP folder + Checking test php files + Creating files folder + Checking test databases...", - ); - return Promise.all([ - glob(path.join(process.cwd(), "src/php", "**/*.php")), - fs.mkdir(path.join(tmpFolder, "files")), - glob(path.join(process.cwd(), "tests", "*.json")), - ]); - }) - .then((results) => { - const glob_php_files = results[0]; - - const php_symbolic_link = glob_php_files.map((from) => { - const endPath = path.relative(path.join(process.cwd(), "src", "php"), from); - const to = path.join(tmpFolder, endPath); - console.log(`Linking ${endPath}...`); - - return fs - .mkdir(path.dirname(to), { recursive: true }) - .then(() => { - return fs.symlink(from, to, "file"); - }) - .then((res) => { - console.log(`Linked ${endPath}`); - return res; - }); - }); - const glob_json_files = results[2]; - console.log("Copying test databases..."); - - const json_prom = glob_json_files.map(async (from) => { - const filename = path.basename(from); - console.log(`Copying ${filename}...`); - const to = path.join(tmpFolder, "files", filename); - return copy(from, to).then((res) => { - console.log(`Copied ${filename}`); - return res; - }); - }); - - const get_test_php_files = glob(path.join(process.cwd(), "tests", "*.php")); - - return Promise.all([get_test_php_files, ...php_symbolic_link, ...json_prom]); - }) - .then((results) => { - console.log("Copying test php config files..."); - const glob_test_php_files = results[0]; - - const php_prom = glob_test_php_files.map((from) => { - const filename = path.basename(from); - const to = path.join(tmpFolder, filename); - console.log(`Linking test ${filename}...`); - - let prom = Promise.resolve(); - if (existsSync(to)) prom = fs.unlink(to); - - return prom - .then(() => fs.symlink(from, to, "file")) - .then((res) => { - console.log(`Linked ${filename}`); - return res; - }); - }); - - return Promise.all(php_prom); - }) - .then(async () => { - // console.log(await (glob(path.join(tmpFolder, '/**/*')))) - const php_server_command = `sh tests/php_server_start.sh ${tmpFolder} ${PORT}`; - console.log('Starting php server with command "' + php_server_command + '"'); - const args = php_server_command.split(" "); - const command = args.shift(); - - child_process.spawn(command, args, { stdio: "ignore", detached: true }).unref(); - - console.log(`Waiting ${PHP_SERVER_START_DELAY}ms for the server to start...`); - return pause(PHP_SERVER_START_DELAY); - }) - .catch((err) => { - console.error("Terrible error happened"); - console.trace(err); - process.exit(1); + execSync("rm -rf /tmp/php-*"); + + const tmpFolder = await mkdtemp(join(tmpdir(), "php-")); + console.log(`Created ${tmpFolder}`); + + console.log( + "Moving PHP folder + Checking test php files + Creating files folder + Checking test databases...", + ); + const [phpPaths, jsonPaths] = await Promise.all([ + glob(join(process.cwd(), PHP_PATH, "**/*.php")), + glob(join(process.cwd(), "tests", "*.json")), + mkdir(join(tmpFolder, "files")), + ]); + + const phpSymlinkProms = phpPaths.map(async (from) => { + const endPath = relative(join(process.cwd(), PHP_PATH), from); + const to = join(tmpFolder, endPath); + console.log(`Linking ${endPath}...`); + + await mkdir(dirname(to), { recursive: true }); + const res = await symlink(from, to, "file"); + console.log(`Linked ${endPath}`); + return res; + }); + + console.log("Copying test databases..."); + + const jsonCopyProms = jsonPaths.map((from) => { + const filename = basename(from); + console.log(`Copying ${filename}...`); + const to = join(tmpFolder, "files", filename); + return copy(from, to).then((res) => { + console.log(`Copied ${filename}`); + return res; }); + }); + + const phpTestPaths = await glob(join(process.cwd(), "tests", "*.php")); + + await Promise.all([...phpSymlinkProms, ...jsonCopyProms]); + + console.log("Copying test PHP config files..."); + + await Promise.all( + phpTestPaths.map(async (from) => { + const filename = basename(from); + const to = join(tmpFolder, filename); + console.log(`Linking test ${filename}...`); + + if (existsSync(to)) await unlink(to); + const res = await symlink(from, to, "file"); + console.log(`Linked ${filename}`); + return res; + }), + ); + + const phpCommand = `sh tests/php_server_start.sh ${tmpFolder} ${PORT}`; + console.log('Starting php server with command "' + phpCommand + '"...'); + const args = phpCommand.split(" "); + const command = args.shift(); + + spawn(command, args, { stdio: "ignore", detached: true }).unref(); + + console.log(`Waiting ${PHP_SERVER_START_DELAY}ms for the server to start...`); + return pause(PHP_SERVER_START_DELAY); } -setup_php(); +setup_php().catch((err) => { + console.error("Terrible error happened"); + console.trace(err); + process.exit(1); +}); /** - * Promisify setTimeout + * Promise-based implementation of setTimeout * @param {Number} ms Timeout in ms - * @param {Function} cb callback function after timeout - * @param {...any} args Optional return arguments * @returns {Promise} */ -const pause = (ms, cb, ...args) => - new Promise((resolve, reject) => { - setTimeout(async () => { - try { - const result = !!cb ? await cb(...args) : undefined; - resolve(result); - } catch (error) { - reject(error); - } - }, ms); - }); +const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/tests/tokens.php b/tests/tokens.php index 2413c9e..eea8d5b 100644 --- a/tests/tokens.php +++ b/tests/tokens.php @@ -4,4 +4,4 @@ 'Test' => 'NeverGonnaGiveYouUp' ); -$db_tokens = array_values($db_tokens_map); \ No newline at end of file +$db_tokens = array_values($db_tokens_map); diff --git a/typings/index.d.ts b/typings/index.d.ts index cd2efe4..16d7690 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,3 +1,5 @@ +import * as NodeFormData from "form-data"; + export type NumberCriteria = | "==" /** Value is equal to the provided value */ | "!=" /** Value is not equal to the provided value */ @@ -22,6 +24,7 @@ export type StringCriteria = export type ArrayCriteria = | "array-contains" /** Value is in the given array */ + | "array-contains-none" /** No value of the array 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 */ @@ -91,7 +94,7 @@ type Field = { [K in keyof T]: T[K] extends P ? K : never; }[keyof T]; -export type EditField = { +export type EditFieldOption = { [K in keyof T]: BaseEditField & ( | { @@ -124,8 +127,8 @@ export type EditField = { } | { field: Field, T>; - operation: "array-slice"; - value: [number, number]; + operation: "array-splice"; + value: [number, number] | [number, number, T[Field, T>][any]]; } ); }[keyof T]; @@ -171,12 +174,19 @@ export type RemoveMethods = Pick< /** ID field not known at add time */ export type Addable = Omit, "id">; -/** ID field can be provided in request */ +/** ID field known at add time */ export type Settable = Addable & { id?: number | string; }; -export class Collection { +/** + * Represents a Firestorm Collection + * @template T Type of collection element + */ +declare class Collection { + /** Name of the Firestorm collection */ + public readonly collectionName: string; + /** * Create a new Firestorm collection instance * @param name - The name of the collection @@ -185,45 +195,47 @@ export class Collection { public constructor(name: string, addMethods?: CollectionMethods); /** - * Get an element from the collection - * @param id - The ID of the element you want to get - * @returns Corresponding value + * Get the sha1 hash of the collection + * - Can be used to compare file content without downloading the file + * @returns The sha1 hash of the file */ - public get(id: string | number): Promise; + public sha1(): string; /** - * Get the sha1 hash of the file - * - Can be used to see if same file content without downloading the file - * @returns The sha1 hash of the file + * Get an element from the collection by its key + * @param key - Key to search + * @returns The found element */ - public sha1(): string; + public get(key: string | number): Promise; + + /** + * Get multiple elements from the collection by their keys + * @param keys - Array of keys to search + * @returns The found elements + */ + public searchKeys(keys: string[] | number[]): Promise; /** * Search through the collection - * @param options - Array of searched options + * @param options - Array of search options * @param random - Random result seed, disabled by default, but can activated with true or a given seed * @returns The found elements */ public search( - options: SearchOption & { id: string | number }>[], + options: SearchOption & { id: string }>[], random?: boolean | number, ): Promise; /** - * Search specific keys through the collection - * @param keys - Array of keys to search - * @returns The found elements - */ - public searchKeys(keys: string[] | number[]): Promise; - - /** - * Returns the whole content of the JSON + * Read the entire collection + * @param original - Disable ID field injection for easier iteration (default false) * @returns The entire collection */ - public readRaw(): Promise>; + public readRaw(original?: boolean): Promise>; /** - * Returns the whole content of the JSON + * Read the entire collection + * - ID values are injected for easier iteration, so this may be different from {@link sha1} * @deprecated Use {@link readRaw} instead * @returns The entire collection */ @@ -231,13 +243,13 @@ export class Collection { /** * Get only selected fields from the collection - * - Essentially an upgraded version of readRaw - * @param option - The option you want to select + * - Essentially an upgraded version of {@link readRaw} + * @param option - The fields you want to select * @returns Selected fields */ public select>( option: SelectOption, - ): Promise>>; + ): Promise>>; /** * Get all distinct non-null values for a given key across a collection @@ -249,7 +261,7 @@ export class Collection { ): Promise ? (F extends true ? T[K] : T[K][]) : T[K][]>; /** - * Get random max entries offset with a given seed + * Read random elements of the collection * @param max - The maximum number of entries * @param seed - The seed to use * @param offset - The offset to use @@ -258,14 +270,16 @@ export class Collection { public random(max: number, seed: number, offset: number): Promise; /** - * Set the entire JSON file contents + * Set the entire content of the collection. + * - Only use this method if you know what you are doing! * @param value - The value to write * @returns Write confirmation */ public writeRaw(value: Record>): Promise; /** - * Set the entire JSON file contents + * Set the entire content of the collection. + * - Only use this method if you know what you are doing! * @deprecated Use {@link writeRaw} instead * @param value - The value to write * @returns Write confirmation @@ -273,76 +287,78 @@ export class Collection { public write_raw(value: Record>): Promise; /** - * Automatically add a value to the JSON file + * Append a value to the collection + * - Only works if autoKey is enabled server-side * @param value - The value (without methods) to add - * @returns The generated ID of the added element + * @returns The generated key of the added element */ public add(value: Addable): Promise; /** - * Automatically add multiple values to the JSON file + * Append multiple values to the collection + * - Only works if autoKey is enabled server-side * @param values - The values (without methods) to add - * @returns The generated IDs of the added elements + * @returns The generated keys of the added elements */ public addBulk(values: Addable[]): Promise; /** - * Remove an element from the collection by its ID - * @param id - The ID of the element you want to remove + * Remove an element from the collection by its key + * @param key - The key of the element you want to remove * @returns Write confirmation */ - public remove(id: string | number): Promise; + public remove(key: string | number): Promise; /** - * Remove multiple elements from the collection by their IDs - * @param ids - The IDs of the elements you want to remove + * Remove multiple elements from the collection by their keys + * @param keys - The keys of the elements you want to remove * @returns Write confirmation */ - public removeBulk(ids: string[] | number[]): Promise; + public removeBulk(keys: string[] | number[]): Promise; /** - * Set a value in the collection by ID - * @param id - The ID of the element you want to edit - * @param value - The value (without methods) you want to edit + * Set a value in the collection by its key + * @param key - The key of the element you want to set + * @param value - The value (without methods) you want to set * @returns Write confirmation */ - public set(id: string | number, value: Settable): Promise; + public set(key: string | number, value: Settable): Promise; /** - * Set multiple values in the collection by their IDs - * @param ids - The IDs of the elements you want to edit - * @param values - The values (without methods) you want to edit + * Set multiple values in the collection by their keys + * @param keys - The keys of the elements you want to set + * @param values - The values (without methods) you want to set * @returns Write confirmation */ - public setBulk(ids: string[] | number[], values: Settable[]): Promise; + public setBulk(keys: string[] | number[], values: Settable[]): Promise; /** - * Edit one field of the collection - * @param edit - The edit object + * Edit an element's field in the collection + * @param option - The edit object * @returns Edit confirmation */ - public editField(edit: EditField>): Promise<{ success: boolean }>; + public editField(option: EditFieldOption>): Promise; /** - * Change one field from multiple elements of the collection - * @param edits - The edit objects + * Edit multiple elements' fields in the collection + * @param options - The edit objects * @returns Edit confirmation */ - public editFieldBulk(edits: EditField>[]): Promise<{ success: boolean[] }>; + public editFieldBulk(options: EditFieldOption>[]): Promise; } -/** Value for the id field when searching content */ +/** Value for the ID field when searching content */ export const ID_FIELD: string; /** - * Change the current Firestorm address + * Change or get the current Firestorm address * @param value - The new Firestorm address * @returns The stored Firestorm address */ export function address(value?: string): string; /** - * Change the current Firestorm token + * Change or get the current Firestorm token * @param value - The new Firestorm write token * @returns The stored Firestorm write token */ @@ -352,16 +368,17 @@ export function token(value?: string): string; * Create a new Firestorm collection instance * @param value - The name of the collection * @param addMethods - Additional methods and data to add to the objects - * @returns The collection + * @returns The collection instance */ export function collection(value: string, addMethods?: CollectionMethods): Collection; /** * Create a temporary Firestorm collection with no methods + * @deprecated Use {@link collection} with no second argument instead * @param table - The table name to get - * @returns The collection + * @returns The table instance */ -export function table(table: string): Promise>; +export function table(table: string): Collection; /** * Firestorm file handler @@ -369,20 +386,20 @@ export function table(table: string): Promise>; export declare const files: { /** * Get a file by its path - * @param path - The file path wanted + * @param path - The wanted file path * @returns File contents */ get(path: string): Promise; /** * Upload a file - * @param form - The form data with path, filename, and file + * @param form - Form data with path, filename, and file * @returns Write confirmation */ - upload(form: FormData): Promise; + upload(form: FormData | NodeFormData): Promise; /** - * Deletes a file by path + * Delete a file by its path * @param path - The file path to delete * @returns Write confirmation */