From 8c4e5d8099d12ec568687728010f8745fe829cf1 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sat, 13 Apr 2024 19:37:33 -0700 Subject: [PATCH 01/30] various fixes (read changelog) - Upgraded __get_request to match __write_data better and have less redundant code - Fixed __write_data overwriting keys sometimes - Failed GET requests now reject directly - added collectionName to typescript - return TypeErrors more consistently --- CHANGELOG.md | 22 ++++- src/index.js | 196 ++++++++++++++++++++---------------------- tests/js-test.spec.js | 17 ++-- tests/php_setup.js | 167 ++++++++++++++++------------------- typings/index.d.ts | 14 ++- 5 files changed, 196 insertions(+), 220 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7cee0..fdf2ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,27 @@ 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. + +### Changed + +- Rejected incorrect parameters are now `TypeError`s instead of regular `Error`s. + +### Fixed + +- Failed GET requests not rejecting the error message properly. +- Certain write commands mutating data internally and affecting parameters outside Firestorm. +- `Collection.searchKeys` and `Collection.values` not returning proper `Error`s sometimes. + +### Removed + +- Redundant `firestorm.table(name)` method, since `firestorm.collection(name)` does exactly the same thing. + +## [1.12.0] - 2024-02-22 ### Added diff --git a/src/index.js b/src/index.js index 9c63f45..d6772f0 100644 --- a/src/index.js +++ b/src/index.js @@ -71,7 +71,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); @@ -104,7 +104,7 @@ class Collection { * @private * @ignore * @param {AxiosPromise} req - Incoming request - * @returns {Object | Object[]} + * @returns {Promise} */ __add_methods(req) { if (!(req instanceof Promise)) req = Promise.resolve(req); @@ -122,27 +122,81 @@ class Collection { * 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) { + __get_request(command, data = {}, objectLike = true) { + const obj = { + collection: this.collectionName, + command: command, + ...data, + }; const request = typeof process === "object" ? axios.get(readAddress(), { - data: data, + data: obj, }) - : axios.post(readAddress(), data); - return this.__extract_data(request); + : 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; + }); + } + + /** + * Generate POST data with provided data + * @private + * @ignore + * @param {string} command - The write command name + * @param {Object} [value] - The value for this command + * @param {boolean} [multiple] - 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, + }; + + // 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; } /** @@ -151,9 +205,7 @@ class Collection { * @returns {Promise} Corresponding value */ get(id) { - return this.__get_request({ - collection: this.collectionName, - command: "get", + return this.__get_request("get", { id: id, }).then((res) => this.__add_methods(res)); } @@ -164,10 +216,8 @@ class Collection { * @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); } /** @@ -178,7 +228,7 @@ class Collection { */ search(searchOptions, random = false) { if (!Array.isArray(searchOptions)) - return Promise.reject(new Error("searchOptions shall be an array")); + return Promise.reject(new TypeError("searchOptions shall be an array")); searchOptions.forEach((searchOption) => { if ( @@ -186,22 +236,20 @@ class Collection { searchOption.criteria === undefined || searchOption.value === undefined ) - return Promise.reject(new Error("Missing fields in searchOptions array")); + return Promise.reject(new TypeError("Missing fields in searchOptions array")); if (typeof searchOption.field !== "string") return Promise.reject( - new Error(`${JSON.stringify(searchOption)} search option field is not a string`), + new TypeError(`${JSON.stringify(searchOption)} 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")); + 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, }; @@ -212,13 +260,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) => { + return this.__get_request("search", params).then((res) => { const arr = Object.entries(res).map(([id, value]) => { value[ID_FIELD_NAME] = id; return value; @@ -234,11 +282,9 @@ class Collection { * @returns {Promise} The found elements */ searchKeys(keys) { - if (!Array.isArray(keys)) return Promise.reject("Incorrect keys"); + if (!Array.isArray(keys)) return Promise.reject(new TypeError("Incorrect keys")); - return this.__get_request({ - collection: this.collectionName, - command: "searchKeys", + return this.__get_request("searchKeys", { search: keys, }).then((res) => { const arr = Object.entries(res).map(([id, value]) => { @@ -255,10 +301,7 @@ class Collection { * @returns {Promise>} The entire collection */ readRaw() { - return this.__get_request({ - collection: this.collectionName, - command: "read_raw", - }).then((data) => { + return this.__get_request("read_raw").then((data) => { // preserve as object Object.keys(data).forEach((key) => { data[key][ID_FIELD_NAME] = key; @@ -286,16 +329,13 @@ class Collection { */ select(selectOption) { if (!selectOption) selectOption = {}; - return this.__get_request({ - collection: this.collectionName, - command: "select", + return this.__get_request("select", { select: selectOption, }).then((data) => { Object.keys(data).forEach((key) => { data[key][ID_FIELD_NAME] = key; this.addMethods(data[key]); }); - return data; }); } @@ -306,14 +346,13 @@ class Collection { * @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) return Promise.reject(new TypeError("Value option must be provided")); + if (typeof valueOption.field !== "string") + return Promise.reject(new TypeError("Field must be a string")); if (valueOption.flatten !== undefined && typeof valueOption.flatten !== "boolean") - return Promise.reject("Flatten must be a boolean"); + return Promise.reject(new TypeError("Flatten must be a boolean")); - return this.__get_request({ - collection: this.collectionName, - command: "values", + return this.__get_request("values", { values: valueOption, }).then((data) => // no ID_FIELD or method injection since no ids are returned @@ -332,30 +371,28 @@ 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) => { @@ -367,55 +404,14 @@ class Collection { }); } - /** - * 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 * @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))); } @@ -440,7 +436,7 @@ 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; }); } @@ -559,16 +555,6 @@ const firestorm = { return new Collection(name, addMethods); }, - /** - * Create a temporary Firestorm collection with no methods - * @template T - * @param {string} name - The table name to get - * @returns {Collection} The collection - */ - table(name) { - return this.collection(name); - }, - /** Value for the id field when researching content */ ID_FIELD: ID_FIELD_NAME, diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 8bc9f58..08b3809 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -274,10 +274,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,7 +282,7 @@ 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(); }) @@ -377,7 +374,7 @@ describe("GET operations", () => { }, ]) .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, "Expected result have not correct length", @@ -698,7 +695,7 @@ describe("GET operations", () => { }); }); -describe("PUT operations", () => { +describe("POST operations", () => { describe("writeRaw operations", () => { it("Rejects when incorrect token", (done) => { firestorm.token("LetsGoToTheMall"); @@ -939,7 +936,7 @@ 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(); }) @@ -954,7 +951,7 @@ describe("PUT operations", () => { base .addBulk(in_value) .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"); @@ -963,7 +960,7 @@ describe("PUT operations", () => { }) .then((results) => { const search_results = results[1]; - expect(search_results).to.be.a("array"); + expect(search_results).to.be.an("array"); expect(search_results).to.have.length(3); const ids_generated = results[0]; diff --git a/tests/php_setup.js b/tests/php_setup.js index a07f7d3..7575130 100644 --- a/tests/php_setup.js +++ b/tests/php_setup.js @@ -1,10 +1,10 @@ -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 { 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_SERVER_START_DELAY = 2000; const PORT = 8000; @@ -14,101 +14,78 @@ 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(path.join(tmpdir(), "php-")); + console.log(`Created ${tmpFolder}`); + + console.log( + "Moving PHP folder + Checking test php files + Creating files folder + Checking test databases...", + ); + const [globPHP, globJSON] = await Promise.all([ + glob(path.join(process.cwd(), "src/php", "**/*.php")), + glob(path.join(process.cwd(), "tests", "*.json")), + mkdir(path.join(tmpFolder, "files")), + ]); + + const symlinkProm = globPHP.map(async (from) => { + const endPath = path.relative(path.join(process.cwd(), "src", "php"), from); + const to = path.join(tmpFolder, endPath); + console.log(`Linking ${endPath}...`); + + await mkdir(path.dirname(to), { recursive: true }); + const res = await symlink(from, to, "file"); + console.log(`Linked ${endPath}`); + return res; + }); + + console.log("Copying test databases..."); + + const jsonProm = globJSON.map((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 globTestPHP = await glob(path.join(process.cwd(), "tests", "*.php")); + + await Promise.all([...symlinkProm, ...jsonProm]); + + console.log("Copying test php config files..."); + + await Promise.all( + globTestPHP.map(async (from) => { + const filename = path.basename(from); + const to = path.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; + }), + ); + // console.log(await (glob(path.join(tmpFolder, '/**/*')))) + 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 diff --git a/typings/index.d.ts b/typings/index.d.ts index cd2efe4..5fa4b99 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -177,6 +177,9 @@ export type Settable = Addable & { }; export class Collection { + /** Name of the Firestorm collection */ + public readonly collectionName: string; + /** * Create a new Firestorm collection instance * @param name - The name of the collection @@ -205,7 +208,7 @@ export class Collection { * @returns The found elements */ public search( - options: SearchOption & { id: string | number }>[], + options: SearchOption & { id: string }>[], random?: boolean | number, ): Promise; @@ -237,7 +240,7 @@ export class Collection { */ public select>( option: SelectOption, - ): Promise>>; + ): Promise>>; /** * Get all distinct non-null values for a given key across a collection @@ -356,13 +359,6 @@ export function token(value?: string): string; */ export function collection(value: string, addMethods?: CollectionMethods): Collection; -/** - * Create a temporary Firestorm collection with no methods - * @param table - The table name to get - * @returns The collection - */ -export function table(table: string): Promise>; - /** * Firestorm file handler */ From ae8d87120e06617805d00897736ed21b5af5e4a2 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sat, 13 Apr 2024 19:52:26 -0700 Subject: [PATCH 02/30] update dependencies --- .gitignore | 6 +- package.json | 8 +- pnpm-lock.yaml | 217 +++++++++++++++++++++--------------------- tests/js-test.spec.js | 3 +- 4 files changed, 117 insertions(+), 117 deletions(-) diff --git a/.gitignore b/.gitignore index 5090528..636b3d7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ node_modules *.log .vscode/ -# NPM RC WITH SENSIBLE INFOS +# NPM RC WITH SENSITIVE INFO \.npmrc ### Node ### @@ -129,5 +129,5 @@ dist .pnp.* #jsdoc -docs/ -.DS_Store +docs/ +.DS_Store diff --git a/package.json b/package.json index 7f7a10f..e9d79e6 100644 --- a/package.json +++ b/package.json @@ -39,19 +39,19 @@ "typings/index.d.ts" ], "dependencies": { - "axios": "^1.6.7" + "axios": "^1.6.8" }, "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/pnpm-lock.yaml b/pnpm-lock.yaml index a421847..bbb3c52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: axios: - specifier: ^1.6.7 - version: 1.6.7 + specifier: ^1.6.8 + version: 1.6.8 devDependencies: chai: @@ -20,8 +20,8 @@ devDependencies: 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,8 +297,8 @@ 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 @@ -477,10 +478,10 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - /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.5 + follow-redirects: 1.15.6 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -491,8 +492,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 +530,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.30001609 + electron-to-chromium: 1.4.736 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) dev: true @@ -564,8 +565,8 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite@1.0.30001588: - resolution: {integrity: sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==} + /caniuse-lite@1.0.30001609: + resolution: {integrity: sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==} dev: true /catharsis@0.9.0: @@ -828,8 +829,8 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true - /electron-to-chromium@1.4.677: - resolution: {integrity: sha512-erDa3CaDzwJOpyvfKhOiJjBVNnMM0qxHq47RheVVwsSQrgBA9ZSGV9kdaOfZDPXcHzhG7lBxhj6A7KvfLJBd6Q==} + /electron-to-chromium@1.4.736: + resolution: {integrity: sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==} dev: true /emoji-regex@8.0.0: @@ -933,8 +934,8 @@ packages: hasBin: true dev: true - /follow-redirects@1.15.5: - resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -1014,16 +1015,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 +1123,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 +1192,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,7 +1319,7 @@ packages: engines: {node: '>=12.0.0'} hasBin: true dependencies: - '@babel/parser': 7.23.9 + '@babel/parser': 7.24.4 '@jsdoc/salty': 0.2.7 '@types/markdown-it': 12.2.3 bluebird: 3.7.2 @@ -1508,8 +1509,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 +1542,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 +1713,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 +2103,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/tests/js-test.spec.js b/tests/js-test.spec.js index 08b3809..39cc72f 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(".."); From 2f5896d3a0ca82f4de56dfad46daf5067badb3c8 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Mon, 15 Apr 2024 18:47:08 -0700 Subject: [PATCH 03/30] more changes, read changelog --- .gitignore | 2 - CHANGELOG.md | 13 +- README.md | 108 +++++++-- package.json | 4 +- {src => php}/.htaccess | 0 php/classes/FileAccess.php | 61 +++++ php/classes/HTTPException.php | 23 ++ {src/php => php}/classes/JSONDatabase.php | 2 +- php/classes/read/random.php | 82 +++++++ {src/php => php}/classes/read/searchArray.php | 7 +- php/config.php | 39 ++++ {src/php => php}/error.php | 0 php/files.php | 208 ++++++++++++++++++ {src/php => php}/get.php | 0 {src/php => php}/index.php | 0 {src/php => php}/log.php | 0 {src/php => php}/post.php | 0 php/tokens.php | 3 + {src/php => php}/utils.php | 0 src/config.php | 33 --- src/index.js | 11 + src/php/classes/FileAccess.php | 61 ----- src/php/classes/HTTPException.php | 24 -- src/php/classes/read/random.php | 83 ------- src/php/files.php | 208 ------------------ src/tokens.php | 7 - tests/js-test.spec.js | 44 +--- tests/php_setup.js | 31 +-- typings/index.d.ts | 58 +++-- 29 files changed, 584 insertions(+), 528 deletions(-) rename {src => php}/.htaccess (100%) create mode 100644 php/classes/FileAccess.php create mode 100644 php/classes/HTTPException.php rename {src/php => php}/classes/JSONDatabase.php (99%) create mode 100644 php/classes/read/random.php rename {src/php => php}/classes/read/searchArray.php (93%) create mode 100644 php/config.php rename {src/php => php}/error.php (100%) create mode 100644 php/files.php rename {src/php => php}/get.php (100%) rename {src/php => php}/index.php (100%) rename {src/php => php}/log.php (100%) rename {src/php => php}/post.php (100%) create mode 100644 php/tokens.php rename {src/php => php}/utils.php (100%) delete mode 100644 src/config.php delete mode 100644 src/php/classes/FileAccess.php delete mode 100644 src/php/classes/HTTPException.php delete mode 100644 src/php/classes/read/random.php delete mode 100644 src/php/files.php delete mode 100644 src/tokens.php diff --git a/.gitignore b/.gitignore index 636b3d7..08d15ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -php/tokens.php -php/config.php files/* node_modules *.log diff --git a/CHANGELOG.md b/CHANGELOG.md index fdf2ff2..74c1816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,21 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Exposed `Collection.collectionName` as a readonly property for TypeScript. +- Exposed `Collection.collectionName` as a readonly property for TypeScript usage. +- Added TypeScript overview to README.md. ### 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. ### Fixed - Failed GET requests not rejecting the error message properly. - Certain write commands mutating data internally and affecting parameters outside Firestorm. - `Collection.searchKeys` and `Collection.values` not returning proper `Error`s sometimes. - -### Removed - -- Redundant `firestorm.table(name)` method, since `firestorm.collection(name)` does exactly the same thing. +- `files.upload` not allowing the `form-data` package's typing of `FormData`. +- Inconsistent use of indentation in PHP files. ## [1.12.0] - 2024-02-22 @@ -96,4 +97,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..eb1ea9f 100644 --- a/README.md +++ b/README.md @@ -64,16 +64,16 @@ A collection takes one required argument and one optional argument: const firestorm = require("firestorm-db"); const userCollection = firestorm.collection("users", (el) => { - el.hello = () => console.log(`${el.name} says hello!`); + 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 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: @@ -152,15 +152,14 @@ Edit objects have an `id` of the element, a `field` to edit, an `operation` with # PHP Part -The PHP files are the ones handling files, read and writes. They also handle `GET` and `POST` requests to manipulate the database. +The PHP files handle files, read, and writes, through `GET` and `POST` requests sent by the JavaScript wrapper. All Firestorm 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 basic files to handle requests can be found and copied [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 the tokens using a `$db_tokens` array. You will use these tokens to write data or read private tables. +- `config.php` stores all of your collections config. This exports a `$database_list` variable with an array of `JSONDatabase` instances. ```php folderPath = './files/'; $tmp->fileName = 'paths'; $tmp->autoKey = true; +$tmp->autoIncrement = false; $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 is `./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 each time (defualt `true`). +- The key in the `$database_list` array is what the collection will be called in JavaScript (this can be different from the JSON filename if needed). -## Firestorm Files +# Firestorm Files File API functions are detailed in the `files.php` PHP script. If you do not want to include this functionality, then just delete this file. You have to add 2 new configuration variables to your `config.php` file: ```php -// whitelist of correct extensions -$authorized_file_extension = array('.txt', '.png'); +// Extension whitelist +$authorized_file_extension = array('.txt', '.png', '.jpg', '.jpeg'); -// subfolder of uploads location, must start with dirname($_SERVER['SCRIPT_FILENAME']) -// to force a subfolder of Firestorm installation +// 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/'; ``` 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. -### File rights +## File rights 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: -``` +```sh sudo chown -R www-data "/path/to/uploads/" ``` -### Upload a file +## 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). 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 @@ -269,7 +275,61 @@ deletePromise .catch((err) => console.error(err)); ``` -## Memory warning +# TypeScript + +Firestorm ships with TypeScript support out of the box. + +## Collection types + +Collections in TypeScript additionally 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 { + id: string; + name: string; + password: string; + pets: string[]; +} + +const userCollection = firestorm.collection("users"); + +const johnDoe = await userCollection.get(123456789); +// type: { name: string, password: string, pets: string[] } +``` + +Methods should also be stored in this interface: + +```ts +import firestorm from "firestorm-db"; +firestorm.address("ADDRESS_VALUE"); + +interface User { + id: string; + name: string; + hello(): string; +} + +const johnDoe = await userCollection.get(123456789); +const hello = johnDoe.hello(); +// type: string +``` + +## 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"; +firestorm.address("ADDRESS_VALUE"); + +const deleteConfirmation = await firestorm.files.delete("/quote.txt"); +// type: firestorm.WriteConfirmation +``` + +# Memory warning Handling big collections can cause memory allocation issues like: @@ -284,11 +344,11 @@ If you encounter a memory allocation issue, you have to allow more memory throug memory_limit = 256M ``` -## API endpoints +# API endpoints -All Firestorm methods correspond to an equivalent Axios request to the relevant PHP file. Read requests are `GET` requests and write requests are `POST` requests with provided JSON data. +If you want to manually send requests without using the JavaScript wrapper, simply make an HTTP request to the relevant PHP file with your content. Read requests are `GET` requests sent to `get.php`, write requests are `POST` requests sent to `post.php` with provided JSON data, and file requests are requests sent to `files.php` with provided form data. -The first keys in the request will always be the same: +The first keys in the request will always be the same, and further keys will depend on the specific method: ```json { diff --git a/package.json b/package.json index e9d79e6..4686978 100644 --- a/package.json +++ b/package.json @@ -39,12 +39,12 @@ "typings/index.d.ts" ], "dependencies": { - "axios": "^1.6.8" + "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.12", "jsdoc": "^4.0.2", "jsdoc-to-markdown": "^8.0.1", 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..ed50c2c --- /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; + } +} \ No newline at end of file diff --git a/php/classes/HTTPException.php b/php/classes/HTTPException.php new file mode 100644 index 0000000..a6ebf57 --- /dev/null +++ b/php/classes/HTTPException.php @@ -0,0 +1,23 @@ +code}]: {$this->message}\n"; + } +} \ No newline at end of file diff --git a/src/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php similarity index 99% rename from src/php/classes/JSONDatabase.php rename to php/classes/JSONDatabase.php index 22482e2..2b8ac8e 100644 --- a/src/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -55,7 +55,7 @@ public function write_raw($content) { return 400; } - // we accept assosiative array as items beacuse they may have an integer key + // we accept associative array as items because they may have an integer key } $content = stringifier($content); diff --git a/php/classes/read/random.php b/php/classes/read/random.php new file mode 100644 index 0000000..c43a1f4 --- /dev/null +++ b/php/classes/read/random.php @@ -0,0 +1,82 @@ += -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/classes/read/searchArray.php b/php/classes/read/searchArray.php similarity index 93% rename from src/php/classes/read/searchArray.php rename to php/classes/read/searchArray.php index 19d0920..84f9477 100644 --- a/src/php/classes/read/searchArray.php +++ b/php/classes/read/searchArray.php @@ -1,7 +1,6 @@ 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; + +/** + * 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/'; \ No newline at end of file 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..27e623c --- /dev/null +++ b/php/files.php @@ -0,0 +1,208 @@ +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 d6772f0..e69d356 100644 --- a/src/index.js +++ b/src/index.js @@ -555,6 +555,17 @@ const firestorm = { return new Collection(name, addMethods); }, + /** + * 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 + */ + table(name) { + return this.collection(name); + }, + /** Value for the id field when researching content */ ID_FIELD: ID_FIELD_NAME, 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/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 @@ - "KGKbkjJKKLMhnkJjkbjkk" -); - -$db_tokens = array_values($db_tokens_map); \ No newline at end of file diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 39cc72f..9a5c6c3 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -116,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; @@ -149,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 @@ -285,9 +279,7 @@ describe("GET operations", () => { 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) => { @@ -303,9 +295,7 @@ 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"))); }); }); @@ -461,9 +451,7 @@ describe("GET operations", () => { unco, ) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); @@ -532,9 +520,7 @@ describe("GET operations", () => { base .select({ fields: unco }) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); }); @@ -562,9 +548,7 @@ describe("GET operations", () => { base .select({ fields: [unco] }) .then(() => done(`[${JSON.stringify(unco)}] value passed`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); }); @@ -648,9 +632,7 @@ describe("GET operations", () => { base .random(unco) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); }); @@ -663,9 +645,7 @@ describe("GET operations", () => { base .random(5, unco) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); }); @@ -685,9 +665,7 @@ describe("GET operations", () => { base .random(5, 69, unco) .then((res) => done(`got ${JSON.stringify(res)} value`)) - .catch(() => { - done(); - }); + .catch(() => done()); }); }); }); diff --git a/tests/php_setup.js b/tests/php_setup.js index 7575130..57d31c7 100644 --- a/tests/php_setup.js +++ b/tests/php_setup.js @@ -1,11 +1,12 @@ const { mkdtemp, mkdir, symlink, unlink } = require("fs").promises; const { existsSync } = require("fs"); -const path = require("path"); +const { join, relative, dirname, basename } = require("path"); const { tmpdir } = require("os"); const { glob } = require("glob"); const copy = require("recursive-copy"); const { execSync, spawn } = require("child_process"); +const PHP_PATH = "php"; const PHP_SERVER_START_DELAY = 2000; const PORT = 8000; @@ -16,24 +17,24 @@ async function setup_php() { // create tmp folder for PHP execSync("rm -rf /tmp/php-*"); - const tmpFolder = await mkdtemp(path.join(tmpdir(), "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 [globPHP, globJSON] = await Promise.all([ - glob(path.join(process.cwd(), "src/php", "**/*.php")), - glob(path.join(process.cwd(), "tests", "*.json")), - mkdir(path.join(tmpFolder, "files")), + glob(join(process.cwd(), PHP_PATH, "**/*.php")), + glob(join(process.cwd(), "tests", "*.json")), + mkdir(join(tmpFolder, "files")), ]); const symlinkProm = globPHP.map(async (from) => { - const endPath = path.relative(path.join(process.cwd(), "src", "php"), from); - const to = path.join(tmpFolder, endPath); + const endPath = relative(join(process.cwd(), PHP_PATH), from); + const to = join(tmpFolder, endPath); console.log(`Linking ${endPath}...`); - await mkdir(path.dirname(to), { recursive: true }); + await mkdir(dirname(to), { recursive: true }); const res = await symlink(from, to, "file"); console.log(`Linked ${endPath}`); return res; @@ -42,16 +43,16 @@ async function setup_php() { console.log("Copying test databases..."); const jsonProm = globJSON.map((from) => { - const filename = path.basename(from); + const filename = basename(from); console.log(`Copying ${filename}...`); - const to = path.join(tmpFolder, "files", filename); + const to = join(tmpFolder, "files", filename); return copy(from, to).then((res) => { console.log(`Copied ${filename}`); return res; }); }); - const globTestPHP = await glob(path.join(process.cwd(), "tests", "*.php")); + const globTestPHP = await glob(join(process.cwd(), "tests", "*.php")); await Promise.all([...symlinkProm, ...jsonProm]); @@ -59,8 +60,8 @@ async function setup_php() { await Promise.all( globTestPHP.map(async (from) => { - const filename = path.basename(from); - const to = path.join(tmpFolder, filename); + const filename = basename(from); + const to = join(tmpFolder, filename); console.log(`Linking test ${filename}...`); if (existsSync(to)) await unlink(to); @@ -69,9 +70,9 @@ async function setup_php() { return res; }), ); - // console.log(await (glob(path.join(tmpFolder, '/**/*')))) + const phpCommand = `sh tests/php_server_start.sh ${tmpFolder} ${PORT}`; - console.log('Starting php server with command "' + phpCommand + '"'); + console.log('Starting php server with command "' + phpCommand + '"...'); const args = phpCommand.split(" "); const command = args.shift(); diff --git a/typings/index.d.ts b/typings/index.d.ts index 5fa4b99..3622dac 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 */ @@ -171,35 +173,35 @@ 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 can be provided in request or omitted */ export type Settable = Addable & { id?: number | string; }; -export class Collection { +export interface Collection { /** Name of the Firestorm collection */ - public readonly collectionName: string; + readonly collectionName: string; /** * Create a new Firestorm collection instance * @param name - The name of the collection * @param addMethods - Additional methods and data to add to the objects */ - public constructor(name: string, addMethods?: CollectionMethods); + new(name: string, addMethods?: CollectionMethods); /** * Get an element from the collection * @param id - The ID of the element you want to get * @returns Corresponding value */ - public get(id: string | number): Promise; + get(id: string | number): Promise; /** * 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 */ - public sha1(): string; + sha1(): string; /** * Search through the collection @@ -207,7 +209,7 @@ export class Collection { * @param random - Random result seed, disabled by default, but can activated with true or a given seed * @returns The found elements */ - public search( + search( options: SearchOption & { id: string }>[], random?: boolean | number, ): Promise; @@ -217,20 +219,20 @@ export class Collection { * @param keys - Array of keys to search * @returns The found elements */ - public searchKeys(keys: string[] | number[]): Promise; + searchKeys(keys: string[] | number[]): Promise; /** * Returns the whole content of the JSON * @returns The entire collection */ - public readRaw(): Promise>; + readRaw(): Promise>; /** * Returns the whole content of the JSON * @deprecated Use {@link readRaw} instead * @returns The entire collection */ - public read_raw(): Promise>; + read_raw(): Promise>; /** * Get only selected fields from the collection @@ -238,7 +240,7 @@ export class Collection { * @param option - The option you want to select * @returns Selected fields */ - public select>( + select>( option: SelectOption, ): Promise>>; @@ -247,7 +249,7 @@ export class Collection { * @param option - Value options * @returns Array of unique values */ - public values, F extends boolean = false>( + values, F extends boolean = false>( option: ValueOption, ): Promise ? (F extends true ? T[K] : T[K][]) : T[K][]>; @@ -258,14 +260,14 @@ export class Collection { * @param offset - The offset to use * @returns The found elements */ - public random(max: number, seed: number, offset: number): Promise; + random(max: number, seed: number, offset: number): Promise; /** * Set the entire JSON file contents * @param value - The value to write * @returns Write confirmation */ - public writeRaw(value: Record>): Promise; + writeRaw(value: Record>): Promise; /** * Set the entire JSON file contents @@ -273,35 +275,35 @@ export class Collection { * @param value - The value to write * @returns Write confirmation */ - public write_raw(value: Record>): Promise; + write_raw(value: Record>): Promise; /** * Automatically add a value to the JSON file * @param value - The value (without methods) to add * @returns The generated ID of the added element */ - public add(value: Addable): Promise; + add(value: Addable): Promise; /** * Automatically add multiple values to the JSON file * @param values - The values (without methods) to add * @returns The generated IDs of the added elements */ - public addBulk(values: Addable[]): Promise; + addBulk(values: Addable[]): Promise; /** * Remove an element from the collection by its ID * @param id - The ID of the element you want to remove * @returns Write confirmation */ - public remove(id: string | number): Promise; + remove(id: string | number): Promise; /** * Remove multiple elements from the collection by their IDs * @param ids - The IDs of the elements you want to remove * @returns Write confirmation */ - public removeBulk(ids: string[] | number[]): Promise; + removeBulk(ids: string[] | number[]): Promise; /** * Set a value in the collection by ID @@ -309,7 +311,7 @@ export class Collection { * @param value - The value (without methods) you want to edit * @returns Write confirmation */ - public set(id: string | number, value: Settable): Promise; + set(id: string | number, value: Settable): Promise; /** * Set multiple values in the collection by their IDs @@ -317,21 +319,21 @@ export class Collection { * @param values - The values (without methods) you want to edit * @returns Write confirmation */ - public setBulk(ids: string[] | number[], values: Settable[]): Promise; + setBulk(ids: string[] | number[], values: Settable[]): Promise; /** * Edit one field of the collection * @param edit - The edit object * @returns Edit confirmation */ - public editField(edit: EditField>): Promise<{ success: boolean }>; + editField(edit: EditField>): Promise<{ success: boolean }>; /** * Change one field from multiple elements of the collection * @param edits - The edit objects * @returns Edit confirmation */ - public editFieldBulk(edits: EditField>[]): Promise<{ success: boolean[] }>; + editFieldBulk(edits: EditField>[]): Promise<{ success: boolean[] }>; } /** Value for the id field when searching content */ @@ -359,6 +361,14 @@ export function token(value?: string): string; */ 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 + */ +export function table(table: string): Promise>; + /** * Firestorm file handler */ @@ -375,7 +385,7 @@ export declare const files: { * @param form - The form data with path, filename, and file * @returns Write confirmation */ - upload(form: FormData): Promise; + upload(form: FormData | NodeFormData): Promise; /** * Deletes a file by path From 0193afa9f65e890bb4acb21163c31221460fb1bd Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Mon, 15 Apr 2024 19:31:16 -0700 Subject: [PATCH 04/30] fix lockfile problems --- pnpm-lock.yaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbb3c52..6f8cf58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: axios: specifier: ^1.6.8 version: 1.6.8 + form-data: + specifier: ^4.0.0 + version: 4.0.0 devDependencies: chai: @@ -16,9 +19,6 @@ devDependencies: docdash: specifier: ^2.0.2 version: 2.0.2 - form-data: - specifier: ^4.0.0 - version: 4.0.0 glob: specifier: ^10.3.12 version: 10.3.12 @@ -477,6 +477,7 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false /axios@1.6.8: resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} @@ -682,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==} @@ -795,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==} @@ -967,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==} @@ -1489,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==} From 9b745d291c3d9a8e1de8de382114e70b032a2616 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Mon, 15 Apr 2024 19:45:59 -0700 Subject: [PATCH 05/30] grammar --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74c1816..15f8293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Failed GET requests not rejecting the error message properly. +- 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`s sometimes. - `files.upload` not allowing the `form-data` package's typing of `FormData`. From bd7af1968a24f34298ee99086690fab34cb24a40 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Mon, 15 Apr 2024 20:02:55 -0700 Subject: [PATCH 06/30] fix public/private class conflict --- CHANGELOG.md | 1 + typings/index.d.ts | 50 +++++++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f8293..ada0056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Collection.searchKeys` and `Collection.values` not returning proper `Error`s sometimes. - `files.upload` not allowing the `form-data` package's typing of `FormData`. - Inconsistent use of indentation in PHP files. +- Collection class being public in TypeScript despite the actual class being private. ## [1.12.0] - 2024-02-22 diff --git a/typings/index.d.ts b/typings/index.d.ts index 3622dac..975577a 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -173,35 +173,39 @@ export type RemoveMethods = Pick< /** ID field not known at add time */ export type Addable = Omit, "id">; -/** ID field can be provided in request or omitted */ +/** ID field known at add time */ export type Settable = Addable & { id?: number | string; }; -export interface Collection { +/** + * Represents a Firestorm Collection + * @template T Type of collection item + */ +declare class Collection { /** Name of the Firestorm collection */ - readonly collectionName: string; + public readonly collectionName: string; /** * Create a new Firestorm collection instance * @param name - The name of the collection * @param addMethods - Additional methods and data to add to the objects */ - new(name: string, addMethods?: CollectionMethods); + 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(id: string | number): Promise; + public get(id: string | number): Promise; /** * 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 */ - sha1(): string; + public sha1(): string; /** * Search through the collection @@ -209,7 +213,7 @@ export interface Collection { * @param random - Random result seed, disabled by default, but can activated with true or a given seed * @returns The found elements */ - search( + public search( options: SearchOption & { id: string }>[], random?: boolean | number, ): Promise; @@ -219,20 +223,20 @@ export interface Collection { * @param keys - Array of keys to search * @returns The found elements */ - searchKeys(keys: string[] | number[]): Promise; + public searchKeys(keys: string[] | number[]): Promise; /** * Returns the whole content of the JSON * @returns The entire collection */ - readRaw(): Promise>; + public readRaw(): Promise>; /** * Returns the whole content of the JSON * @deprecated Use {@link readRaw} instead * @returns The entire collection */ - read_raw(): Promise>; + public read_raw(): Promise>; /** * Get only selected fields from the collection @@ -240,7 +244,7 @@ export interface Collection { * @param option - The option you want to select * @returns Selected fields */ - select>( + public select>( option: SelectOption, ): Promise>>; @@ -249,7 +253,7 @@ export interface Collection { * @param option - Value options * @returns Array of unique values */ - values, F extends boolean = false>( + public values, F extends boolean = false>( option: ValueOption, ): Promise ? (F extends true ? T[K] : T[K][]) : T[K][]>; @@ -260,14 +264,14 @@ export interface Collection { * @param offset - The offset to use * @returns The found elements */ - random(max: number, seed: number, offset: number): Promise; + public random(max: number, seed: number, offset: number): Promise; /** * Set the entire JSON file contents * @param value - The value to write * @returns Write confirmation */ - writeRaw(value: Record>): Promise; + public writeRaw(value: Record>): Promise; /** * Set the entire JSON file contents @@ -275,35 +279,35 @@ export interface Collection { * @param value - The value to write * @returns Write confirmation */ - write_raw(value: Record>): Promise; + public write_raw(value: Record>): Promise; /** * Automatically add a value to the JSON file * @param value - The value (without methods) to add * @returns The generated ID of the added element */ - add(value: Addable): Promise; + public add(value: Addable): Promise; /** * Automatically add multiple values to the JSON file * @param values - The values (without methods) to add * @returns The generated IDs of the added elements */ - addBulk(values: Addable[]): Promise; + 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 * @returns Write confirmation */ - remove(id: string | number): Promise; + public remove(id: string | number): Promise; /** * Remove multiple elements from the collection by their IDs * @param ids - The IDs of the elements you want to remove * @returns Write confirmation */ - removeBulk(ids: string[] | number[]): Promise; + public removeBulk(ids: string[] | number[]): Promise; /** * Set a value in the collection by ID @@ -311,7 +315,7 @@ export interface Collection { * @param value - The value (without methods) you want to edit * @returns Write confirmation */ - set(id: string | number, value: Settable): Promise; + public set(id: string | number, value: Settable): Promise; /** * Set multiple values in the collection by their IDs @@ -319,21 +323,21 @@ export interface Collection { * @param values - The values (without methods) you want to edit * @returns Write confirmation */ - setBulk(ids: string[] | number[], values: Settable[]): Promise; + public setBulk(ids: string[] | number[], values: Settable[]): Promise; /** * Edit one field of the collection * @param edit - The edit object * @returns Edit confirmation */ - editField(edit: EditField>): Promise<{ success: boolean }>; + public editField(edit: EditField>): Promise<{ success: boolean }>; /** * Change one field from multiple elements of the collection * @param edits - The edit objects * @returns Edit confirmation */ - editFieldBulk(edits: EditField>[]): Promise<{ success: boolean[] }>; + public editFieldBulk(edits: EditField>[]): Promise<{ success: boolean[] }>; } /** Value for the id field when searching content */ From 0f6565be44436e1d71b5f161351d0de651157c96 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Tue, 16 Apr 2024 17:51:41 -0700 Subject: [PATCH 07/30] remove trailing whitespace --- php/log.php | 4 ++-- php/post.php | 20 ++++++++++---------- tests/config.php | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/php/log.php b/php/log.php index 0eab682..8b31c0a 100644 --- a/php/log.php +++ b/php/log.php @@ -10,7 +10,7 @@ public static function addLog($message) { $fp = fopen($path, 'a'); fwrite($fp, $now->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/php/post.php b/php/post.php index 47cbf7e..8991ab2 100644 --- a/php/post.php +++ b/php/post.php @@ -15,7 +15,7 @@ if(!$inputJSON) http_error(400, 'No JSON body provided'); - + // pre_dump($inputJSON); // exit(); @@ -26,7 +26,7 @@ if(file_exists('./tokens.php') == false) http_error(501, 'Admin didn\'t implemented tokens.php file'); -// add tokens +// add tokens require_once('./tokens.php'); if(!$db_tokens) @@ -48,22 +48,22 @@ // trying things try { - + // checking good collection if(!array_key_exists($collection, $database_list)) http_error(404, 'Collection not found: ' . $collection); - + $db = $database_list[$collection]; $command = check_key_json('command', $inputJSON); if($command === false) http_error(400, 'No command provided'); - + $commands_available = ['write_raw', 'add', 'addBulk', 'remove', 'removeBulk', 'set', 'setBulk', 'editField', 'editFieldBulk']; if(!in_array($command, $commands_available)) http_error(404, 'Command not found: ' . $command . '. Available commands: ' . join(', ', $commands_available)); - + $valueKeyName = ($command != 'setBulk' && $command != 'addBulk') ? 'value' : 'values'; $value = check_key_json($valueKeyName, $inputJSON, false); @@ -95,7 +95,7 @@ $dbKey = check_key_json('key', $inputJSON); if($dbKey === false) http_error(400, 'No key provided'); - + $db->set($dbKey, $value); http_success('Successful ' . $command . ' command'); break; @@ -103,7 +103,7 @@ $dbKey = check_key_json('keys', $inputJSON, false); if($dbKey === false) http_error(400, 'No keys provided'); - + $db->setBulk($dbKey, $value); http_success('Successful ' . $command . ' command'); break; @@ -111,14 +111,14 @@ $res = $db->editField($value); if($res === false) http_error(400, 'Incorrect data provided'); - + http_message($res, 'success', 200); break; case 'editFieldBulk': $res = $db->editFieldBulk($value); if($res === false) http_error(400, 'Incorrect data provided'); - + http_message($res, 'success', 200); break; default: diff --git a/tests/config.php b/tests/config.php index 630cc08..dde5a88 100644 --- a/tests/config.php +++ b/tests/config.php @@ -19,12 +19,12 @@ $dbName = $prop[0]; $autoKey = $prop[1]; $autoKeyIncrement = count($prop) > 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; } From 7b21d155be995aaa2fce6f8286540ca54fffa462 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Wed, 17 Apr 2024 16:03:24 -0700 Subject: [PATCH 08/30] fix array-splice conflicts, typos, style --- CHANGELOG.md | 11 ++- php/classes/FileAccess.php | 10 +-- php/classes/HTTPException.php | 4 +- php/classes/JSONDatabase.php | 144 +++++++++++++++---------------- php/classes/read/random.php | 18 ++-- php/classes/read/searchArray.php | 2 - php/files.php | 62 ++++++------- php/get.php | 37 ++++---- php/post.php | 39 ++++----- php/utils.php | 4 +- pnpm-lock.yaml | 20 ++--- src/index.js | 12 +-- tests/js-test.spec.js | 2 +- tests/php_setup.js | 30 ++----- typings/index.d.ts | 14 +-- 15 files changed, 194 insertions(+), 215 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ada0056..e34c946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Exposed `Collection.collectionName` as a readonly property for TypeScript usage. -- Added TypeScript overview to README.md. +- TypeScript overview to README.md. +- Optional replacement argument for `array-splice` edit fields. ### Changed @@ -24,8 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Certain write commands mutating data internally and affecting parameters outside Firestorm. - `Collection.searchKeys` and `Collection.values` not returning proper `Error`s sometimes. - `files.upload` not allowing the `form-data` package's typing of `FormData`. -- Inconsistent use of indentation in PHP files. -- Collection class being public in TypeScript despite the actual class being private. +- 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`. ## [1.12.0] - 2024-02-22 @@ -36,7 +39,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. diff --git a/php/classes/FileAccess.php b/php/classes/FileAccess.php index ed50c2c..bd25e42 100644 --- a/php/classes/FileAccess.php +++ b/php/classes/FileAccess.php @@ -8,7 +8,7 @@ public static function read($filepath, $waitLock = false, $default = null) { // exit if couldn't find file if ($file === false) { - if($default == null) + if ($default == null) throw new Exception('Could not open file: ' . $filepath); // set default value @@ -16,7 +16,7 @@ public static function read($filepath, $waitLock = false, $default = null) { } // if no file, puts default value inside - if($file === false) { + if ($file === false) { file_put_contents($fileobj['filepath'], $fileobj['content'], LOCK_EX); $file = fopen($filepath, 'rb'); } @@ -24,7 +24,7 @@ public static function read($filepath, $waitLock = false, $default = null) { $fileobj['fd'] = $file; // if want the lock, we wait for the shared lock - if($waitLock) { + if ($waitLock) { $lock = flock($file, LOCK_SH); if (!$lock) { fclose($file); @@ -41,7 +41,7 @@ public static function read($filepath, $waitLock = false, $default = null) { $fileobj['content'] = $string; // if no wait you can close the file - if(!$waitLock) + if (!$waitLock) fclose($file); return $fileobj; @@ -51,7 +51,7 @@ public static function write($fileobj) { flock($fileobj['fd'], LOCK_UN); fclose($fileobj['fd']); - if(!is_writable($fileobj['filepath'])) { + if (!is_writable($fileobj['filepath'])) { throw new HTTPException("PHP script can't write to file. Check permission, group and owner.", 400); } diff --git a/php/classes/HTTPException.php b/php/classes/HTTPException.php index a6ebf57..5fe4b62 100644 --- a/php/classes/HTTPException.php +++ b/php/classes/HTTPException.php @@ -5,11 +5,11 @@ class HTTPException extends Exception { public function __construct($message, $code = 400, Throwable $previous = null) { $type_message = gettype($message); - if($type_message != 'string') + if ($type_message != 'string') throw new Exception("Incorrect message type for HTTPException constructor, expected string, got " . $type_message); $type_code = gettype($code); - if($type_code != 'integer') + if ($type_code != 'integer') throw new Exception("Incorrect code type for HTTPException constructor, expected string, got " . $type_code); // assign everything diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index 2b8ac8e..408aea4 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -25,18 +25,18 @@ public function write_raw($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); + if (in_array($content_type, $incorrect_types)) { + throw new HTTPException('write_raw value cannot 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)) { + 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); + if (in_array($item_type, $incorrect_types)) { + throw new HTTPException('write_raw item cannot be a ' . $item_type, 400); return 400; } } @@ -50,8 +50,8 @@ public function write_raw($content) { // 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); + if (in_array($item_type, $incorrect_types)) { + throw new HTTPException('write_raw item with key' . $key . ' item cannot be a ' . $item_type, 400); return 400; } @@ -60,9 +60,8 @@ public function write_raw($content) { $content = stringifier($content); - // fix empty raw content - //be cause php parses {} as array(0) - if($content === '[]') + // fix empty raw content because php parses {} as array(0) + if ($content === '[]') $content = '{}'; return file_put_contents($this->fullPath(), $content, LOCK_EX); @@ -91,7 +90,7 @@ public function read($waitLock = false) { public function get($key) { $obj = $this->read(); - if(!$obj || array_key_exists('content', $obj) == false || array_key_exists(strval($key), $obj['content']) == false) + if (!$obj || array_key_exists('content', $obj) == false || array_key_exists(strval($key), $obj['content']) == false) return null; $res = array($key => $obj['content'][$key]); @@ -99,24 +98,25 @@ public function get($key) { } public function set($key, $value) { - if($key === null or $value === null) { /// === fixes the empty array == comparaison + // "===" fixes the empty array "==" comparison + if ($key === null or $value === null) { throw new HTTPException("Key or value are null", 400); } $key_var_type = gettype($key); - if($key_var_type != 'string' and $key_var_type != 'integer') + 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') + 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)) + 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 + // set it at the corresponding value $obj = $this->read(true); $obj['content'][$key] = json_decode(json_encode($value), true); return $this->write($obj); @@ -125,7 +125,7 @@ public function set($key, $value) { public function setBulk($keys, $values) { // we verify that our keys are in an array $key_var_type = gettype($keys); - if($key_var_type != 'array') + if ($key_var_type != 'array') throw new Exception('Incorect keys type'); // else set it at the corresponding value @@ -134,10 +134,10 @@ public function setBulk($keys, $values) { // 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++) { + 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') + 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]); @@ -149,7 +149,7 @@ public function setBulk($keys, $values) { } private function newLastKey($arr) { - if($this->autoIncrement) { + 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; @@ -166,10 +166,10 @@ private function newLastKey($arr) { 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))) + 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) + if ($this->autoKey == false) throw new Exception('Autokey disabled'); // else set it at the corresponding value @@ -184,27 +184,27 @@ public function add($value) { } public function addBulk($values) { - if($values !== array() and $values == NULL) + 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))) + 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))) + 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) + if ($this->autoKey == false) throw new Exception('Autokey disabled'); // veriify that values is an array with number indices - if(array_assoc($values)) + if (array_assoc($values)) throw new Exception('Wanted sequential array'); // else set it at the correspongding value @@ -227,7 +227,7 @@ public function addBulk($values) { } public function remove($key) { - if(gettype($key) != 'string') + if (gettype($key) != 'string') throw new HTTPException("remove value must be a string", 400); $obj = $this->read(true); @@ -236,15 +236,15 @@ public function remove($key) { } public function removeBulk($keys) { - if($keys !== array() and $keys == NULL) + if ($keys !== array() and $keys == NULL) throw new HTTPException('null-like keys not accepted', 400); - if(gettype($keys) !== 'array' or array_assoc($keys)) + if (gettype($keys) !== 'array' or array_assoc($keys)) throw new HTTPException('keys must be an array', 400); - for($i = 0; $i < count($keys); $i++) { + 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') + 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]); @@ -290,7 +290,7 @@ public function search($conditions, $random = false) { } error_reporting(error_reporting() + E_NOTICE); - if($el != NULL && array_key_exists($field, $el) && array_key_exists('criteria', $condition) && array_key_exists('value', $condition)) { + if ($el != NULL && array_key_exists($field, $el) && array_key_exists('criteria', $condition) && array_key_exists('value', $condition)) { $criteria = $condition['criteria']; $value = $condition['value']; @@ -300,7 +300,7 @@ public function search($conditions, $random = false) { // get concerned field type $fieldType = gettype($concernedField); - if($criteria == 'array-contains' || $criteria == 'array-contains-any') { + if ($criteria == 'array-contains' || $criteria == 'array-contains-any') { $ignoreCase = array_key_exists('ignoreCase', $condition) && !!$condition['ignoreCase']; } @@ -435,16 +435,16 @@ public function search($conditions, $random = false) { $el = $el_root; } - if($add) { + if ($add) { $res[$key] = $el_root; } } - if($random !== false) { + if ($random !== false) { $seed = false; - if(is_array($random) && array_key_exists('seed', $random)) { + 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"); + if (!is_int($rawSeed)) throw new HTTPException("Seed not an integer value for random search result"); $seed = intval($rawSeed); } $res = chooseRandom($res, $seed); @@ -457,13 +457,13 @@ public function searchKeys($searchedKeys) { $obj = $this->read(); $res = array(); - if(gettype($searchedKeys) != 'array') + if (gettype($searchedKeys) != 'array') return $res; foreach($searchedKeys as $key) { $key = strval($key); - if(array_key_exists($key, $obj['content'])) { + if (array_key_exists($key, $obj['content'])) { $res[$key] = $el = $obj['content'][$key]; } } @@ -477,51 +477,50 @@ public function editField($editObj) { // MANDATORY REFERENCE to edit directly: PHP 5+ private function __edit(&$obj, $editObj) { - - if(!is_object($editObj)) + if (!is_object($editObj)) return false; // id required - if(!check($editObj['id'])) + if (!check($editObj['id'])) return false; $id = $editObj['id']; // id string or integer - if(gettype($id) != 'string' and gettype($id) != '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])) + if (!array_key_exists($id, $obj['content']) || !check($obj['content'][$id])) return false; // field required - if(!check($editObj['field'])) + if (!check($editObj['field'])) return false; $field = $editObj['field']; // field is a string - if(gettype($field) != 'string') + if (gettype($field) != 'string') return false; // operation required - if(!check($editObj['operation'])) + 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'])) + // set, append, array-push, array-delete, array-splice + if (in_array($operation, ['set', 'append', 'array-push', 'array-delete', 'array-splice']) 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')) + if (!isset($obj['content'][$id][$field]) and ($operation != 'set' and $operation != 'remove' and $operation != 'array-push')) return false; switch($operation) { @@ -533,14 +532,14 @@ private function __edit(&$obj, $editObj) { return true; case 'append': // check type string - if(gettype($obj['content'][$id][$field]) != 'string' or gettype($value) != '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') + if (gettype($obj['content'][$id][$field]) != 'boolean') return false; $obj['content'][$id][$field] = !$obj['content'][$id][$field]; @@ -548,14 +547,14 @@ private function __edit(&$obj, $editObj) { case 'increment': case 'decrement': // check type number - if(gettype($obj['content'][$id][$field]) != 'integer' and gettype($obj['content'][$id][$field]) != 'double') + 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 + 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 @@ -567,15 +566,15 @@ private function __edit(&$obj, $editObj) { return true; case 'array-push': // create it if not here - if(!isset($obj['content'][$id][$field])) + if (!isset($obj['content'][$id][$field])) $obj['content'][$id][$field] = array(); // check if our field array - if(gettype($obj['content'][$id][$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])) + if (array_assoc($obj['content'][$id][$field])) return false; array_push($obj['content'][$id][$field], $value); @@ -584,29 +583,30 @@ private function __edit(&$obj, $editObj) { case 'array-delete': // check if our field array - if(gettype($obj['content'][$id][$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])) + if (array_assoc($obj['content'][$id][$field])) return false; // value must be integer - if(gettype($value) != '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])) + 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') + // 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') return false; - array_splice($obj['content'][$id][$field], $value[0], $value[1]); + 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: @@ -618,7 +618,7 @@ private function __edit(&$obj, $editObj) { public function editFieldBulk($objArray) { // need sequential array - if(array_assoc($objArray)) + if (array_assoc($objArray)) return false; $arrayResult = array(); @@ -638,10 +638,10 @@ 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'); + 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'); + if ($verif_fields === false) throw new HTTPException('Incorrect fields type, expected an array'); $fields = $selectObj['fields']; $i = 0; $fields_count = count($fields); @@ -649,7 +649,7 @@ public function select($selectObj) { $verif_fields = gettype($fields[$i]) === 'string'; ++$i; } - if(!$verif_fields) throw new HTTPException('fields field incorrect, expected an array of string'); + if (!$verif_fields) throw new HTTPException('fields field incorrect, expected an array of string'); $obj = $this->read(); @@ -658,7 +658,7 @@ public function select($selectObj) { foreach ($json as $key => $value) { $result[$key] = array(); foreach ($fields as $field) { - if(array_key_exists($field, $value)) $result[$key][$field] = $value[$field]; + if (array_key_exists($field, $value)) $result[$key][$field] = $value[$field]; } } @@ -669,7 +669,7 @@ public function values($valueObj) { if (!array_key_exists('field', $valueObj)) throw new HTTPException('Missing required field field'); - if(!is_string($valueObj['field'])) + if (!is_string($valueObj['field'])) throw new HTTPException('Incorrect field type, expected a string'); if (array_key_exists('flatten', $valueObj)) { diff --git a/php/classes/read/random.php b/php/classes/read/random.php index c43a1f4..60e99ce 100644 --- a/php/classes/read/random.php +++ b/php/classes/read/random.php @@ -11,21 +11,21 @@ function make_seed() { function random($params, $class) { $hasMax = array_key_exists('max', $params); $max = $hasMax ? $params['max'] : -1; - if($hasMax && (gettype($max) !== 'integer' || $max < -1)) throw new HTTPException('Expected integer >= -1 for the max'); + if ($hasMax && (gettype($max) !== 'integer' || $max < -1)) throw new HTTPException('Expected integer >= -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'); + 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'); + 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'); + if ($hasSeed && gettype($seed) !== 'integer') throw new HTTPException('Expected integer for the seed'); $json = $class->read()['content']; @@ -37,15 +37,15 @@ function chooseRandom($json, $seed = false, $max = -1, $offset = 0) { $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 ($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; + if ($max == -1 || $max > $keys_length) $max = $keys_length; // set random seed just before starting picking - if($seed !== false) mt_srand($seed); + 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) { + for ($i = 0; $i < $offset; ++$i) { $index = mt_rand(0, $keys_length - 1); array_splice($keys, $index, 1); @@ -73,7 +73,7 @@ function chooseRandom($json, $seed = false, $max = -1, $offset = 0) { // get objects from keys selected $result = array(); - for($i = 0; $i < count($keys_selected); ++$i) { + for ($i = 0; $i < count($keys_selected); ++$i) { $key = strval($keys_selected[$i]); $result[$key] = $json[$key]; } diff --git a/php/classes/read/searchArray.php b/php/classes/read/searchArray.php index 84f9477..e26b724 100644 --- a/php/classes/read/searchArray.php +++ b/php/classes/read/searchArray.php @@ -33,8 +33,6 @@ function array_contains_any($concernedField, $value, $ignoreCase = false) { $val_i = $val_i + 1; } - if($val_i == count($value)) { - } $add = $tmp; } else { $add = false; diff --git a/php/files.php b/php/files.php index 27e623c..f4088fe 100644 --- a/php/files.php +++ b/php/files.php @@ -7,19 +7,19 @@ require_once('./config.php'); -if(!$STORAGE_LOCATION) http_error(501, 'Developer forgot the $STORAGE_LOCATION'); +if (!$STORAGE_LOCATION) http_error(501, 'Developer forgot the $STORAGE_LOCATION'); // import useful functions require_once('./utils.php'); require_once('./log.php'); $method = sec($_SERVER['REQUEST_METHOD']); -if($method !== 'GET' && $method !== 'POST' && $method !== 'DELETE') { +if ($method !== 'GET' && $method !== 'POST' && $method !== 'DELETE') { http_error(400, 'Incorrect request type, expected GET, POST or DELETE, not ' . $method); } -if($method === 'POST') { +if ($method === 'POST') { /** * post is creation * You need a token @@ -30,20 +30,20 @@ // add tokens require_once('./tokens.php'); - if(!$db_tokens) + if (!$db_tokens) http_error(501, 'Developer is dumb and forgot to create tokens'); - if(!$authorized_file_extension) + if (!$authorized_file_extension) http_error(501, 'Developer is dumb and forgot to create $authorized_file_extension'); $token = p('token'); // verifying token - if($token === false) http_error(400, 'No token provided'); - if(!in_array($token, $db_tokens)) http_error(403, 'Invalid token'); + if ($token === false) http_error(400, 'No token provided'); + if (!in_array($token, $db_tokens)) http_error(403, 'Invalid token'); $path = trim(p('path')); - if($path === false) http_error(400, 'No path provided'); + if ($path === false) http_error(400, 'No path provided'); // check path lower than me $relativePath = removeDots($path); @@ -51,9 +51,9 @@ $myPath = removeDots($STORAGE_LOCATION); // avoid hacks to write script or files unauthorized - if(strpos($absolutePath, $myPath) !== 0) http_error(403, 'Path not authorized'); + if (strpos($absolutePath, $myPath) !== 0) http_error(403, 'Path not authorized'); // no php script allowed - if(str_ends_with($absolutePath, '.php') === 0) http_error(403, 'Cannot write php scripts'); + if (str_ends_with($absolutePath, '.php') === 0) http_error(403, 'Cannot write PHP scripts'); $extensionFound = false; $i = 0; @@ -62,18 +62,18 @@ $i = $i + 1; } - if(!$extensionFound) { + if (!$extensionFound) { http_error(403, 'Extension not allowed'); } - if(!check($_FILES) || !check($_FILES['file'])) { + if (!check($_FILES) || !check($_FILES['file'])) { http_error(400, 'No file provided or the provided file did not contain an original name'); } // overwrite parameter $overwrite = !!p('overwrite'); - if(!$overwrite && file_exists($absolutePath)) http_error(403, 'File already exists'); + if (!$overwrite && file_exists($absolutePath)) http_error(403, 'File already exists'); $uploaddir = dirname($absolutePath); @@ -87,31 +87,31 @@ http_error(500, "PHP script can't create folder " . $uploaddir . ", check permission, group and owner."); } - if(!check($_FILES) || !check($_FILES['file'])) http_error(400, 'No actual file was given'); + if (!check($_FILES) || !check($_FILES['file'])) http_error(400, 'No actual file was given'); $tmpName = $_FILES['file']['tmp_name']; // eventually write the file - if(move_uploaded_file($tmpName, $absolutePath)) { + if (move_uploaded_file($tmpName, $absolutePath)) { http_success('Written file successfully to ' . $relativePath); } else { http_error(500, "PHP script can't write to file, check permission, group and owner."); } die(); -} else if($method === 'GET') { +} else if ($method === 'GET') { $path = trim(g('path')); - if($path === false) http_error(400, 'No path provided'); + if ($path === false) http_error(400, 'No path provided'); // check path lower than me $absolutePath = removeDots($STORAGE_LOCATION . $path); $myPath = removeDots($STORAGE_LOCATION); // avoid hacks to write script or files unauthorized - if(strpos($absolutePath, $myPath) !== 0) http_error(403, 'Path not authorized'); + if (strpos($absolutePath, $myPath) !== 0) http_error(403, 'Path not authorized'); // no php script allowed - if(substr_compare($absolutePath, ".php", -strlen(".php"), null, true) === 0) http_error(403, 'Cannot read php scripts'); + if (substr_compare($absolutePath, ".php", -strlen(".php"), null, true) === 0) http_error(403, 'Cannot read PHP scripts'); - if(!file_exists($absolutePath)) http_error(404, 'File not found'); + if (!file_exists($absolutePath)) http_error(404, 'File not found'); try { // try to read the image @@ -140,7 +140,7 @@ die(); } -} else if($method === 'DELETE') { +} else if ($method === 'DELETE') { /** * delete is deletion * You need a token @@ -150,11 +150,11 @@ // add tokens require_once('./tokens.php'); - if(!$db_tokens) + if (!$db_tokens) http_error(501, 'Developer is dumb and forgot to create tokens'); $data = json_decode(file_get_contents('php://input'), true); - if($data === false) http_error(400, 'Could not parse input data'); + if ($data === false) http_error(400, 'Could not parse input data'); try { $token = $data['token']; @@ -163,11 +163,11 @@ } // verifying token - if($token === false) http_error(400, 'No token provided'); - if(!in_array($token, $db_tokens)) http_error(403, 'Invalid token'); + if ($token === false) http_error(400, 'No token provided'); + if (!in_array($token, $db_tokens)) http_error(403, 'Invalid token'); $path = trim($data['path']); - if($path === false) http_error(400, 'No path provided'); + if ($path === false) http_error(400, 'No path provided'); // check path lower than me $relativePath = removeDots($path); @@ -175,13 +175,13 @@ $myPath = removeDots($STORAGE_LOCATION); // avoid hacks to write script or files unauthorized - if(strpos($absolutePath, $myPath) !== 0) http_error(403, 'Path not authorized'); + if (strpos($absolutePath, $myPath) !== 0) http_error(403, 'Path not authorized'); - if(!file_exists($absolutePath)) http_error(404, 'File not found'); + if (!file_exists($absolutePath)) http_error(404, 'File not found'); $is_deleted = unlink($absolutePath); - if($is_deleted) http_success('File successfully deleted'); + if ($is_deleted) http_success('File successfully deleted'); http_error(500, 'Deletion failed'); } @@ -190,7 +190,7 @@ function p($var) { try { - if(!check($_POST[$var])) return false; + if (!check($_POST[$var])) return false; } catch (Throwable $th) { return false; } @@ -199,7 +199,7 @@ function p($var) { function g($var) { try { - if(!check($_GET[$var])) return false; + if (!check($_GET[$var])) return false; } catch (Throwable $th) { return false; } diff --git a/php/get.php b/php/get.php index 022b9ff..ce13faf 100644 --- a/php/get.php +++ b/php/get.php @@ -12,33 +12,29 @@ require_once('./log.php'); $method = sec($_SERVER['REQUEST_METHOD']); -if($method !== 'GET' && $method !== 'POST') { +if ($method !== 'GET' && $method !== 'POST') { http_error(400, 'Incorrect request type, expected GET or POST, not ' . $method); } $inputJSON = json_decode(file_get_contents('php://input'), true); -if(!$inputJSON) +if (!$inputJSON) http_error(400, 'No JSON body provided'); -// pre_dump($inputJSON); -// exit(); - $collection = check_key_json('collection', $inputJSON); -if(!$collection) +if (!$collection) http_error(400, 'No collection provided'); -if(file_exists('./config.php') == false) - http_error(501, 'Admin didn\'t implemented config.php file'); +if (file_exists('./config.php') == false) + http_error(501, 'Developer didn\'t implement a config.php file'); // import db config require_once('./config.php'); -// trying things try { // checking good collection -if(!array_key_exists($collection, $database_list)) +if (!array_key_exists($collection, $database_list)) http_error(404, 'Collection not found: ' . $collection); /** @@ -47,15 +43,12 @@ $db = $database_list[$collection]; $command = check_key_json('command', $inputJSON); -if(!$command) +if (!$command) http_error(400, 'No command provided'); $commands_available = ['read_raw', 'get', 'search', 'searchKeys', 'select', 'random', 'sha1', 'values']; -// var_dump($command); -// exit(); - -if(!in_array($command, $commands_available)) +if (!in_array($command, $commands_available)) http_error(404, 'Command not found: ' . $command . '. Available commands: ' . join(', ', $commands_available)); switch($command) { @@ -72,11 +65,11 @@ $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) + if (!$result) http_error(404, 'get failed on collection ' . $collection . ' with key ' . $id); http_response(stringifier($result)); @@ -85,7 +78,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 +88,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 +98,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/php/post.php b/php/post.php index 8991ab2..ec0e6bd 100644 --- a/php/post.php +++ b/php/post.php @@ -7,41 +7,38 @@ cors(); $method = sec($_SERVER['REQUEST_METHOD']); -if($method === 'GET') { +if ($method === 'GET') { http_error(400, 'Incorrect request type, expected POST, not ' . $method); } $inputJSON = json_decode(file_get_contents('php://input'), true); -if(!$inputJSON) +if (!$inputJSON) http_error(400, 'No JSON body provided'); -// pre_dump($inputJSON); -// exit(); - $token = check_key_json('token', $inputJSON); -if(!$token) +if (!$token) http_error(400, 'No token provided'); -if(file_exists('./tokens.php') == false) - http_error(501, 'Admin didn\'t implemented tokens.php file'); +if (file_exists('./tokens.php') == false) + http_error(501, 'Developer didn\'t implement a tokens.php file'); // add tokens require_once('./tokens.php'); -if(!$db_tokens) +if (!$db_tokens) http_error(400, 'Developer is dumb and forgot to create tokens'); // verifying token -if(!in_array($token, $db_tokens)) +if (!in_array($token, $db_tokens)) http_error(403, 'Invalid token'); $collection = check_key_json('collection', $inputJSON); -if(!check($collection)) +if (!check($collection)) http_error(400, 'No collection provided'); -if(file_exists('./config.php') == false) - http_error(501, 'Admin didn\'t implemented config.php file'); +if (file_exists('./config.php') == false) + http_error(501, 'Developer didn\'t implement a config.php file'); // import db config require_once('./config.php'); @@ -50,24 +47,24 @@ try { // checking good collection -if(!array_key_exists($collection, $database_list)) +if (!array_key_exists($collection, $database_list)) http_error(404, 'Collection not found: ' . $collection); $db = $database_list[$collection]; $command = check_key_json('command', $inputJSON); -if($command === false) +if ($command === false) http_error(400, 'No command provided'); $commands_available = ['write_raw', 'add', 'addBulk', 'remove', 'removeBulk', 'set', 'setBulk', 'editField', 'editFieldBulk']; -if(!in_array($command, $commands_available)) +if (!in_array($command, $commands_available)) http_error(404, 'Command not found: ' . $command . '. Available commands: ' . join(', ', $commands_available)); $valueKeyName = ($command != 'setBulk' && $command != 'addBulk') ? 'value' : 'values'; $value = check_key_json($valueKeyName, $inputJSON, false); -if($value === false) +if ($value === false) http_error(400, 'No ' . $valueKeyName . ' provided'); switch($command) { @@ -93,7 +90,7 @@ 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); @@ -101,7 +98,7 @@ 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); @@ -109,14 +106,14 @@ break; case 'editField': $res = $db->editField($value); - if($res === false) + if ($res === false) http_error(400, 'Incorrect data provided'); http_message($res, 'success', 200); break; case 'editFieldBulk': $res = $db->editFieldBulk($value); - if($res === false) + if ($res === false) http_error(400, 'Incorrect data provided'); http_message($res, 'success', 200); diff --git a/php/utils.php b/php/utils.php index 3c84092..7edbbef 100644 --- a/php/utils.php +++ b/php/utils.php @@ -7,7 +7,7 @@ function pre_dump($val) { } function check($var) { - if(isset($var) and !empty($var)) { + if (isset($var) and !empty($var)) { return true; } return false; @@ -57,7 +57,7 @@ function array_sequential(array $arr) { } function stringifier($obj, $depth = 1) { - if($depth == 0) { + if ($depth == 0) { return json_encode($obj); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f8cf58..60b239e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -304,8 +304,8 @@ packages: '@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 @@ -531,8 +531,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001609 - electron-to-chromium: 1.4.736 + 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 @@ -566,8 +566,8 @@ packages: engines: {node: '>=10'} dev: true - /caniuse-lite@1.0.30001609: - resolution: {integrity: sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==} + /caniuse-lite@1.0.30001610: + resolution: {integrity: sha512-QFutAY4NgaelojVMjY63o6XlZyORPaLfyMnsl3HgnWdJUcX6K0oaJymHjH8PT5Gk7sTm8rvC/c5COUQKXqmOMA==} dev: true /catharsis@0.9.0: @@ -825,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.736: - resolution: {integrity: sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==} + /electron-to-chromium@1.4.739: + resolution: {integrity: sha512-koRkawXOuN9w/ymhTNxGfB8ta4MRKVW0nzifU17G1UwTWlBg0vv7xnz4nxDnRFSBe9nXMGRgICcAzqXc0PmLeA==} dev: true /emoji-regex@8.0.0: @@ -1324,7 +1324,7 @@ packages: hasBin: true dependencies: '@babel/parser': 7.24.4 - '@jsdoc/salty': 0.2.7 + '@jsdoc/salty': 0.2.8 '@types/markdown-it': 12.2.3 bluebird: 3.7.2 catharsis: 0.9.0 diff --git a/src/index.js b/src/index.js index e69d356..8bcfc01 100644 --- a/src/index.js +++ b/src/index.js @@ -520,7 +520,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 */ @@ -533,7 +533,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 */ @@ -566,7 +566,7 @@ const firestorm = { 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, /** @@ -579,7 +579,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) { @@ -595,7 +595,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) { @@ -610,7 +610,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 diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 9a5c6c3..0f13deb 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -1311,7 +1311,7 @@ describe("POST 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({ diff --git a/tests/php_setup.js b/tests/php_setup.js index 57d31c7..451d89f 100644 --- a/tests/php_setup.js +++ b/tests/php_setup.js @@ -23,13 +23,13 @@ async function setup_php() { console.log( "Moving PHP folder + Checking test php files + Creating files folder + Checking test databases...", ); - const [globPHP, globJSON] = await Promise.all([ + const [phpPaths, jsonPaths] = await Promise.all([ glob(join(process.cwd(), PHP_PATH, "**/*.php")), glob(join(process.cwd(), "tests", "*.json")), mkdir(join(tmpFolder, "files")), ]); - const symlinkProm = globPHP.map(async (from) => { + const phpSymlinkProms = phpPaths.map(async (from) => { const endPath = relative(join(process.cwd(), PHP_PATH), from); const to = join(tmpFolder, endPath); console.log(`Linking ${endPath}...`); @@ -42,7 +42,7 @@ async function setup_php() { console.log("Copying test databases..."); - const jsonProm = globJSON.map((from) => { + const jsonCopyProms = jsonPaths.map((from) => { const filename = basename(from); console.log(`Copying ${filename}...`); const to = join(tmpFolder, "files", filename); @@ -52,14 +52,14 @@ async function setup_php() { }); }); - const globTestPHP = await glob(join(process.cwd(), "tests", "*.php")); + const phpTestPaths = await glob(join(process.cwd(), "tests", "*.php")); - await Promise.all([...symlinkProm, ...jsonProm]); + await Promise.all([...phpSymlinkProms, ...jsonCopyProms]); - console.log("Copying test php config files..."); + console.log("Copying test PHP config files..."); await Promise.all( - globTestPHP.map(async (from) => { + phpTestPaths.map(async (from) => { const filename = basename(from); const to = join(tmpFolder, filename); console.log(`Linking test ${filename}...`); @@ -89,20 +89,8 @@ setup_php().catch((err) => { }); /** - * 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/typings/index.d.ts b/typings/index.d.ts index 975577a..082ba81 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -126,8 +126,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]; @@ -344,14 +344,14 @@ declare class Collection { 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 */ @@ -379,20 +379,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 | NodeFormData): Promise; /** - * Deletes a file by path + * Delete a file by its path * @param path - The file path to delete * @returns Write confirmation */ From 17c4776ca3692630faabcc37155324784d8a3b54 Mon Sep 17 00:00:00 2001 From: Evorp <75297863+3vorp@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:11:27 -0700 Subject: [PATCH 09/30] quick type fix --- typings/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index 082ba81..3c8d069 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -371,7 +371,7 @@ export function collection(value: string, addMethods?: CollectionMethods): * @param table - The table name to get * @returns The collection */ -export function table(table: string): Promise>; +export function table(table: string): Collection; /** * Firestorm file handler From 3966df30dd7379dca48cb260f15d35e9b971c981 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Thu, 18 Apr 2024 17:48:40 -0700 Subject: [PATCH 10/30] fix platform-specific nested key issue --- CHANGELOG.md | 1 + php/classes/JSONDatabase.php | 10 +++-- tests/js-test.spec.js | 81 +++++++++++------------------------- 3 files changed, 31 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e34c946..2d28a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ## [1.12.0] - 2024-02-22 diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index 408aea4..dcdad38 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -276,19 +276,21 @@ public function search($conditions, $random = false) { // get condition $condition = $conditions[$condition_index]; - // get condition fields extracted + // extract field $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)) { + // 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_ind = $field_ind + 1; + $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']; diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 0f13deb..639510c 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -384,7 +384,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([ { @@ -398,9 +398,7 @@ describe("GET operations", () => { expect(res.length).to.equal(0); done(); }) - .catch((err) => { - done(err); - }); + .catch((err) => done(err)); }); it("can find correct nested value", (done) => { base @@ -436,7 +434,8 @@ describe("GET operations", () => { }); }); - let incorrect = [null, "gg", ""]; // undefined works because random becomes default parameter false, so false works too + // undefined works because random becomes default parameter false, so false works too + const incorrect = [null, "gg", ""]; incorrect.forEach((unco) => { it(`${JSON.stringify(unco)} seed rejects`, (done) => { base @@ -507,14 +506,12 @@ 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" }]; + const incorrect = [undefined, null, false, 5, 12.5, "gg", { toto: "tata" }]; incorrect.forEach((unco) => { it(`${JSON.stringify(unco)} value`, (done) => { base @@ -530,12 +527,8 @@ describe("GET operations", () => { it(`${JSON.stringify(val)} value`, (done) => { base .select({ fields: val }) - .then(() => { - done(); - }) - .catch((err) => { - done(err); - }); + .then(() => done()) + .catch((err) => done(err)); }); }); }); @@ -565,7 +558,7 @@ describe("GET operations", () => { } }); }); - let selectResult = results[1]; + const selectResult = results[1]; Object.keys(selectResult).forEach((k) => { delete selectResult[k][firestorm.ID_FIELD]; }); @@ -581,27 +574,21 @@ 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()); }); }); @@ -610,17 +597,13 @@ describe("GET operations", () => { 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")); }); @@ -639,7 +622,7 @@ describe("GET operations", () => { 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 + const incorrect = [null, false, "gg", 5.5]; // undefined works because then seed is automatic incorrect.forEach((unco) => { it(`${JSON.stringify(unco)} value`, (done) => { base @@ -659,7 +642,7 @@ 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 + const incorrect = [null, false, "gg", 5.5, -1]; // undefined works because then offset is 0 incorrect.forEach((unco) => { it(`${JSON.stringify(unco)} value`, (done) => { base @@ -679,9 +662,7 @@ describe("POST operations", () => { base .writeRaw({}) - .then((res) => { - done(res); - }) + .then((res) => done(res)) .catch((err) => { if ("response" in err && err.response.status == 403) { done(); @@ -689,9 +670,7 @@ describe("POST operations", () => { } done(new Error("Should return 403")); }) - .finally(() => { - firestorm.token(TOKEN); - }); + .finally(() => firestorm.token(TOKEN)); }); describe("You must give a correct value", () => { @@ -711,9 +690,7 @@ describe("POST operations", () => { 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"); @@ -738,9 +715,7 @@ describe("POST operations", () => { it("but it can write an empty content : {}", (done) => { base .writeRaw({}) - .then(() => { - done(); - }) + .then(() => done()) .catch((err) => { console.trace(err); done(err); @@ -761,9 +736,7 @@ describe("POST 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(); @@ -793,9 +766,7 @@ describe("POST operations", () => { expect(id).to.equals(String(parseInt(last_id) + 1)); done(); }) - .catch((err) => { - done(err); - }); + .catch((err) => done(err)); }); describe("It should not accept incorrect values", () => { @@ -835,12 +806,8 @@ describe("POST operations", () => { it(`${index === 0 ? "Empty object" : "Complex object"} should fulfill`, (done) => { base .add(co) - .then(() => { - done(); - }) - .catch((err) => { - done(err); - }); + .then(() => done()) + .catch((err) => done(err)); }); }); }); From 7f3243077f83405ead8accbb933f982371282a0a Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Fri, 19 Apr 2024 20:40:32 -0700 Subject: [PATCH 11/30] optional JSONDatabase constructor, shuffle readme --- CHANGELOG.md | 1 + README.md | 112 +++++++++++++++++++---------------- php/classes/JSONDatabase.php | 7 +++ php/config.php | 5 ++ tests/config.php | 24 ++++---- 5 files changed, 87 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d28a6f..4b4a0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Exposed `Collection.collectionName` as a readonly property for TypeScript usage. - TypeScript overview to README.md. - Optional replacement argument for `array-splice` edit fields. +- Optional constructor for the `JSONDatabase` PHP class to reduce repetitive code. ### Changed diff --git a/README.md b/README.md index eb1ea9f..f9b861b 100644 --- a/README.md +++ b/README.md @@ -14,28 +14,29 @@ _Self hosted Firestore-like database with API endpoints based on micro bulk oper ## Installation +Installing the JavaScript wrapper is as simple as running: +```sh +npm install firestorm-db ``` -npm install --save firestorm-db -``` +To install the PHP backend used to actually query data, copy the sample [php](./php/) folder to your hosting platform and edit the configuration files. +More information about configuring Firestorm server-side is given in the [PHP](#php-part) section. # JavaScript Part -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 API. -## 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 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: @@ -53,23 +54,24 @@ userCollection .catch((err) => console.error(err)); ``` -### Collection constructor +## Create your first Collection -A collection takes one required argument and one optional argument: +A Firestorm collection takes one required argument and one optional argument: -- The name of the collection as a `String`. +- 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. ```js const firestorm = require("firestorm-db"); const userCollection = firestorm.collection("users", (el) => { + // inject a method into the element 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 +// assumes you have a 'users' table with a printable field called 'name' const johnDoe = await userCollection.get(123456789); // gives { name: "John Doe", hello: Function } @@ -148,18 +150,18 @@ Edit objects have an `id` of the element, a `field` to edit, an `operation` with | `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. | -
+Various other methods and constants exist in the JavaScript wrapper, which will make more sense once you learn what's actually happening behind the scenes. # PHP Part -The PHP files handle files, read, and writes, through `GET` and `POST` requests sent by the JavaScript wrapper. All Firestorm methods correspond to an equivalent Axios request to the relevant PHP file. +The PHP files handle files, read, and writes, through `GET` and `POST` requests sent by the JavaScript wrapper. All JavaScript methods correspond to an equivalent Axios request to the relevant PHP file. ## PHP setup -The basic files to handle requests can be found and copied [here](./php/). The two files that need editing are `tokens.php` and `config.php`. +The server-side files to handle requests can be found and copied [here](./php/). The two files that need editing are `tokens.php` and `config.php`. -- `tokens.php` contains the tokens using a `$db_tokens` array. You will use these tokens to write data or read private tables. -- `config.php` stores all of your collections config. This exports 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 wrapper. +- `config.php` stores all of your collections. This file needs to declare a `$database_list` array of `JSONDatabase` instances. ```php folderPath = './files/'; -$tmp->fileName = 'users'; -$tmp->autoKey = false; +$tmp->fileName = 'paths'; +$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; -$tmp->autoIncrement = false; $database_list[$tmp->fileName] = $tmp; ?> ``` -- The database will be stored in `/.json` (default folder is `./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 each time (defualt `true`). -- The key in the `$database_list` array is what the collection will be called in JavaScript (this can be different from the JSON filename if needed). +- 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 will be called in JavaScript in the Collection constructor (this can be different from the JSON filename if needed). # Firestorm Files -File API functions are detailed in the `files.php` PHP script. If you do not want to include this functionality, then just delete this file. +Firestorm's file APIs are implemented in `files.php`. If you don't need file-related features, then simply delete this file. -You have to add 2 new configuration variables to your `config.php` file: +To work with files server-side, you need two new configuration variables in `config.php`: ```php // Extension whitelist @@ -205,23 +207,21 @@ $authorized_file_extension = array('.txt', '.png', '.jpg', '.jpeg'); $STORAGE_LOCATION = dirname($_SERVER['SCRIPT_FILENAME']) . '/uploads/'; ``` -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. - -## File rights - -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: +Additionally, since the PHP scripts create folders and files, the script will fail if the PHP user doesn't have write permissions. +You can give Firestorm rights to a folder with the following command: ```sh sudo chown -R www-data "/path/to/uploads/" ``` +From there, you can use the functions in `firestorm.files` (detailed below) from the JavaScript wrapper. +If your upload folder is accessible from a server URL, you can directly use its address to retrieve the file. + ## 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 mistakes. @@ -246,7 +246,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. ```js const firestorm = require("firestorm-db"); @@ -261,7 +261,7 @@ getPromise ## 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 rather than gets it. ```js const firestorm = require("firestorm-db"); @@ -275,7 +275,7 @@ deletePromise .catch((err) => console.error(err)); ``` -# TypeScript +# TypeScript Support Firestorm ships with TypeScript support out of the box. @@ -300,7 +300,7 @@ const johnDoe = await userCollection.get(123456789); // type: { name: string, password: string, pets: string[] } ``` -Methods should also be stored in this interface: +Methods should also be stored in this interface, and will be filtered out from write operations: ```ts import firestorm from "firestorm-db"; @@ -312,9 +312,21 @@ interface User { hello(): string; } -const johnDoe = await userCollection.get(123456789); -const hello = johnDoe.hello(); -// type: string +const johnDoe = await userCollection.get(123456789, (el) => { + // interface types should agree with injected methods + el.hello = () => `${el.name} says hello!`; + return el; +}); +const hello = johnDoe.hello(); // type: string + +await userCollection.add({ + id: "123456788", + name: "Mary Doe", + // error: Object literal may only specify known properties, and 'hello' does not exist in type 'Addable'. + hello() { + return "Mary Doe says hello!" + } +}) ``` ## Additional types @@ -329,7 +341,7 @@ const deleteConfirmation = await firestorm.files.delete("/quote.txt"); // type: firestorm.WriteConfirmation ``` -# Memory warning +# Memory Warning Handling big collections can cause memory allocation issues like: @@ -344,11 +356,11 @@ If you encounter a memory allocation issue, you have to allow more memory throug memory_limit = 256M ``` -# API endpoints +# API Endpoints -If you want to manually send requests without using the JavaScript wrapper, simply make an HTTP request to the relevant PHP file with your content. Read requests are `GET` requests sent to `get.php`, write requests are `POST` requests sent to `post.php` with provided JSON data, and file requests are requests sent to `files.php` with provided form data. +If you want to manually send requests without using the JavaScript wrapper, simply make an HTTP request to the relevant PHP file with your content. Read requests are `GET` requests sent to `/get.php`, write requests are `POST` requests sent to `/post.php` with provided JSON data, and file requests are requests sent to `/files.php` with provided form data. -The first keys in the request will always be the same, and further keys will depend on the specific method: +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 { diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index dcdad38..e864bf1 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -16,6 +16,13 @@ class JSONDatabase { public $autoKey = true; public $autoIncrement = true; + public function __construct(string $fileName = 'db', bool $autoKey = true, bool $autoIncrement = true) { + // if no/some args provided they just fall back to their defaults + $this->fileName = $fileName; + $this->autoKey = $autoKey; + $this->autoIncrement = $autoIncrement; + } + public function fullPath() { return $this->folderPath . $this->fileName . $this->fileExt; } diff --git a/php/config.php b/php/config.php index 1776dca..67bbc56 100644 --- a/php/config.php +++ b/php/config.php @@ -26,6 +26,11 @@ // 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. diff --git a/tests/config.php b/tests/config.php index dde5a88..a6ddb99 100644 --- a/tests/config.php +++ b/tests/config.php @@ -15,18 +15,18 @@ $STORAGE_LOCATION = dirname($_SERVER['SCRIPT_FILENAME']) . '/uploads/'; $database_list = array(); -foreach($props as $prop) { - $dbName = $prop[0]; - $autoKey = $prop[1]; - $autoKeyIncrement = count($prop) > 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; -} + +// test with constructor/optional args +$database_list['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"; From 3944c63b7d4ce7843f309f5fd80776a24c115dc6 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sat, 20 Apr 2024 09:38:52 -0700 Subject: [PATCH 12/30] quick fix to delete $props --- tests/config.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/config.php b/tests/config.php index a6ddb99..b12b89b 100644 --- a/tests/config.php +++ b/tests/config.php @@ -3,11 +3,6 @@ require_once('./utils.php'); require_once('./classes/JSONDatabase.php'); -$props = array( - array('house', false), - array('base', true, true), -); - // whitelist of correct extensions $authorized_file_extension = array('.txt', '.png', '.jpg', '.jpeg'); // subfolder of uploads location, must start with dirname($_SERVER['SCRIPT_FILENAME']) From 1fd6b920572d1f0a1c81bdcfd1e58e0eace48877 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sat, 20 Apr 2024 09:43:55 -0700 Subject: [PATCH 13/30] quick readme fix --- README.md | 2 ++ php/config.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f9b861b..6767f26 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,8 @@ $database_list[$tmp->fileName] = $tmp; - `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 will be called in JavaScript in the Collection constructor (this can be different from the JSON filename if needed). +The sample files additionally have some comments on how to edit Collections. + # Firestorm Files Firestorm's file APIs are implemented in `files.php`. If you don't need file-related features, then simply delete this file. diff --git a/php/config.php b/php/config.php index 67bbc56..8154b9e 100644 --- a/php/config.php +++ b/php/config.php @@ -9,7 +9,7 @@ /** * Example server-side collection creation: * The class has most things set by default, so this is intentionally verbose - * If you're working with multiple collections, it's probably easier to iterate over an array of options to create new instances + * For adding multiple collections, you can declare them directly in the array constructor. */ $db = new JSONDatabase; From cefdf0a922ea1be85f1b793d6e0356b846728720 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sat, 20 Apr 2024 20:03:50 -0700 Subject: [PATCH 14/30] "advanced" readme section --- CHANGELOG.md | 1 + README.md | 162 ++++++++++++++++++++++++++++++--------------- src/index.js | 12 ++-- typings/index.d.ts | 2 +- 4 files changed, 117 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b4a0f8..e9eca18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - TypeScript overview to README.md. - Optional replacement argument for `array-splice` edit fields. - Optional constructor for the `JSONDatabase` PHP class to reduce repetitive code. +- "Advanced" section to the README for previously undocumented features. ### Changed diff --git a/README.md b/README.md index 6767f26..e5ec02f 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,15 @@ _Self hosted Firestore-like database with API endpoints based on micro bulk oper ## Installation -Installing the JavaScript wrapper is as simple as running: +Installing the JavaScript client is as simple as running: + ```sh npm install firestorm-db ``` -To install the PHP backend used to actually query data, copy the sample [php](./php/) folder to your hosting platform and edit the configuration files. -More information about configuring Firestorm server-side is given in the [PHP](#php-part) section. -# 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 simply an [Axios](https://www.npmjs.com/package/axios) wrapper of the PHP API. @@ -30,6 +31,7 @@ The JavaScript [index.js](./src/index.js) file is simply an [Axios](https://www. First, set your API address (and your writing token if needed) using the `address()` and `token()` functions: ```js +// only needed in Node.js, including the script tag in a browser is enough otherwise. const firestorm = require("firestorm-db"); firestorm.address("http://example.com/path/to/firestorm/root/"); @@ -39,24 +41,11 @@ firestorm.address("http://example.com/path/to/firestorm/root/"); 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"); - -// all methods return promises -userCollection - .readRaw() - .then((res) => console.log(res)) - .catch((err) => console.error(err)); -``` +Now you can use Firestorm to its full potential. ## Create your first Collection -A Firestorm 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. A Firestorm collection 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. @@ -65,13 +54,13 @@ A Firestorm collection takes one required argument and one optional argument: const firestorm = require("firestorm-db"); const userCollection = firestorm.collection("users", (el) => { - // inject a method into the element + // 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; }); -// assumes you have a 'users' table with a printable field called 'name' +// all methods return promises const johnDoe = await userCollection.get(123456789); // gives { name: "John Doe", hello: Function } @@ -80,7 +69,7 @@ johnDoe.hello(); // "John Doe says hello!" Available methods for a collection: -### Read operations +## Read operations | Name | Parameters | Description | | ----------------------------- | ------------------------------------------------------------| --------------------------------------------------------------------------------------------------------------------- | @@ -97,7 +86,7 @@ The search method can take one or more options to filter entries in a collection 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 +## All search options available | Criteria | Types allowed | Description | | ---------------------- | ----------------------------- | --------------------------------------------------------------------------------- | @@ -120,7 +109,7 @@ Not all criteria are available depending the field type. There are more options | `'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 +## Write operations | Name | Parameters | Description | | ----------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------- | @@ -134,7 +123,7 @@ Not all criteria are available depending the field type. There are more options | 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 +## Edit field operations Edit objects have an `id` of the element, a `field` to edit, an `operation` with what to do to this field, and a possible `value`. Here is a list of operations: @@ -150,17 +139,17 @@ Edit objects have an `id` of the element, a `field` to edit, an `operation` with | `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. | -Various other methods and constants exist in the JavaScript wrapper, which will make more sense once you learn what's actually happening behind the scenes. +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 handle files, read, and writes, through `GET` and `POST` requests sent by the JavaScript wrapper. All JavaScript methods correspond to an equivalent Axios request to the relevant PHP file. +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 server-side files to handle requests can be found and copied [here](./php/). The two files that need editing are `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` contains writing tokens declared in a `$db_tokens` array. These correspond to the tokens used with `firestorm.token()` in the JavaScript wrapper. +- `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` array of `JSONDatabase` instances. ```php @@ -216,8 +205,7 @@ You can give Firestorm rights to a folder with the following command: sudo chown -R www-data "/path/to/uploads/" ``` -From there, you can use the functions in `firestorm.files` (detailed below) from the JavaScript wrapper. -If your upload folder is accessible from a server URL, you can directly use its address to retrieve the file. +From there, you can use the functions in `firestorm.files` (detailed below) from the JavaScript client. ## Upload a file @@ -239,6 +227,7 @@ form.append("path", "/quote.txt"); 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 @@ -248,7 +237,7 @@ uploadPromise ## Get a file -`firestorm.files.get` takes a file's direct URL location or its content as its parameter. +`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. ```js const firestorm = require("firestorm-db"); @@ -257,13 +246,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 -`firestorm.files.delete` has the same interface as `firestorm.files.get`, but as the name suggests, it deletes the file rather than gets it. +`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"); @@ -302,7 +291,7 @@ const johnDoe = await userCollection.get(123456789); // type: { name: string, password: string, pets: string[] } ``` -Methods should also be stored in this interface, and will be filtered out from write operations: +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"; @@ -314,17 +303,19 @@ interface User { hello(): string; } -const johnDoe = await userCollection.get(123456789, (el) => { +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({ id: "123456788", name: "Mary Doe", - // error: Object literal may only specify known properties, and 'hello' does not exist in type 'Addable'. + // error: 'hello' does not exist in type 'Addable'. hello() { return "Mary Doe says hello!" } @@ -337,38 +328,103 @@ Additional types exist for search criteria options, write method return types, c ```ts import firestorm from "firestorm-db"; -firestorm.address("ADDRESS_VALUE"); +const address = firestorm.address("ADDRESS_VALUE"); +// type: string const deleteConfirmation = await firestorm.files.delete("/quote.txt"); // type: firestorm.WriteConfirmation ``` -# Memory Warning +# Advanced features -Handling big collections can cause memory allocation issues like: +## `ID_FIELD` and its meaning -``` -Fatal error: -Allowed memory size of 134217728 bytes exhausted (tried to allocate 32360168 bytes) -``` +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 key names disappearing. Additionally, it can be used in the method adder in the constructor, and is convenient for collections with manual key names. + +```js +import firestorm from "firestorm-db"; +firestorm.address("ADDRESS_VALUE"); + +const userCollection = firestorm.collection("users", (el) => { + el.basicInfo = () => `${el.name} (${el[firestorm.ID_FIELD]})`; + return el; +}); -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: +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) ``` -memory_limit = 256M + +As it's entirely a JavaScript construct, `ID_FIELD` values will never be in your collection. + +## Combining Collections + +Using add methods in the constructor, you can link multiple collections together. + +```js +import firestorm from "firestorm-db"; +firestorm.address("ADDRESS_VALUE"); + +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 stores by 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(); ``` -# API Endpoints +## Manually sending data + +Each operation type requests a different file. In the JavaScript client, the corresponding file gets appended onto your base Firestorm address. -If you want to manually send requests without using the JavaScript wrapper, simply make an HTTP request to the relevant PHP file with your content. Read requests are `GET` requests sent to `/get.php`, write requests are `POST` requests sent to `/post.php` with provided JSON data, and file requests are requests sent to `/files.php` with provided form data. +- 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 warning + +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/src/index.js b/src/index.js index 8bcfc01..53792f9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ +// browser check try { if (typeof process === "object") var axios = require("axios").default; -} catch (_error) {} +} catch {} /** * @typedef {Object} SearchOption @@ -161,8 +162,8 @@ class Collection { * @private * @ignore * @param {string} command - The write command name - * @param {Object} [value] - The value for this command - * @param {boolean} [multiple] - Need to delete multiple + * @param {Object} [value] - The value for the command + * @param {boolean} [multiple] - Used to delete multiple * @returns {Object} Write data object */ __write_data(command, value = undefined, multiple = false) { @@ -628,8 +629,7 @@ const firestorm = { }, }; +// browser check try { if (typeof process === "object") module.exports = firestorm; -} catch (_error) { - // normal browser -} +} catch {} diff --git a/typings/index.d.ts b/typings/index.d.ts index 3c8d069..b451999 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -180,7 +180,7 @@ export type Settable = Addable & { /** * Represents a Firestorm Collection - * @template T Type of collection item + * @template T Type of collection element */ declare class Collection { /** Name of the Firestorm collection */ From 945865a322d780df18b52cf9a4fea691e65f21df Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sun, 21 Apr 2024 17:11:31 -0700 Subject: [PATCH 15/30] clean up jsdoc --- CHANGELOG.md | 1 + README.md | 199 ++++++++++++++++++++--------------- jsdoc.json | 2 +- php/classes/JSONDatabase.php | 6 +- src/index.js | 162 ++++++++++++++-------------- typings/index.d.ts | 108 ++++++++++--------- 6 files changed, 259 insertions(+), 219 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9eca18..010452d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ### Fixed diff --git a/README.md b/README.md index e5ec02f..68236dc 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,21 @@

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_ -## Installation +# Installation Installing the JavaScript client is as simple as running: @@ -45,7 +50,7 @@ Now you can use Firestorm to its full potential. ## Create your first Collection -Firestorm is based around the concept of a `Collection`, which is akin to an SQL table or Firestore document. A Firestorm 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. A Firestorm collection takes one required argument and one optional argument in its constructor: - 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. @@ -67,77 +72,75 @@ const johnDoe = await userCollection.get(123456789); 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 | +| Name | Parameters | Description | +| ------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| sha1() | none | Get the sha1 hash of the file. Can be used to compare file content without downloading the JSON. | +| readRaw() | none | Read the entire collection. ID values are injected for easier iteration, so this may be different from sha1. | +| 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-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() | 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 | +| 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 operations Edit objects have an `id` of the element, a `field` to edit, an `operation` with what to do to this field, and a possible `value`. Here is a list of operations: -| Operation | 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 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 array element by index. Check the PHP [array_splice](https://www.php.net/manual/function.array-splice) documentation for more info. | +| `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. @@ -150,7 +153,7 @@ Firestorm's PHP files handle files, read, and writes, through `GET` and `POST` r 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` 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` array of `JSONDatabase` instances. +- `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 = 'paths'; +$tmp->fileName = 'orders'; $tmp->autoKey = true; $tmp->autoIncrement = false; @@ -173,7 +176,6 @@ $tmp = new JSONDatabase('users', false); $tmp->folderPath = './files/'; $database_list[$tmp->fileName] = $tmp; -?> ``` - The database will be stored in `/.json` (default folder: `./files/`). @@ -181,7 +183,17 @@ $database_list[$tmp->fileName] = $tmp; - `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 will be called in JavaScript in the Collection constructor (this can be different from the JSON filename if needed). -The sample files additionally have some comments on how to edit Collections. +If you're working with multiple collections, it's easier to initialize them all in the array constructor directly: + +```php +// config.php + new JSONDatabase('orders', true), + 'users' => new JSONDatabase('users', false), +) +``` # Firestorm Files @@ -237,7 +249,7 @@ uploadPromise ## Get a file -`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. +`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"); @@ -272,14 +284,13 @@ Firestorm ships with TypeScript support out of the box. ## Collection types -Collections in TypeScript additionally 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`. +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 { - id: string; name: string; password: string; pets: string[]; @@ -298,7 +309,6 @@ import firestorm from "firestorm-db"; firestorm.address("ADDRESS_VALUE"); interface User { - id: string; name: string; hello(): string; } @@ -313,7 +323,6 @@ const johnDoe = await userCollection.get(123456789); const hello = johnDoe.hello(); // type: string await userCollection.add({ - id: "123456788", name: "Mary Doe", // error: 'hello' does not exist in type 'Addable'. hello() { @@ -335,18 +344,15 @@ const deleteConfirmation = await firestorm.files.delete("/quote.txt"); // type: firestorm.WriteConfirmation ``` -# Advanced features +# 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 key names disappearing. Additionally, it can be used in the method adder in the constructor, and is convenient for collections with manual key names. +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 -import firestorm from "firestorm-db"; -firestorm.address("ADDRESS_VALUE"); - const userCollection = firestorm.collection("users", (el) => { el.basicInfo = () => `${el.name} (${el[firestorm.ID_FIELD]})`; return el; @@ -362,14 +368,35 @@ 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 configuration will disable `add` operations: + +```php +$database_list['users'] = new JSONDatabase('users', false); +``` + +```js +const userCollection = firestorm.collection("users"); +// Error: Autokey 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)); +``` + ## Combining Collections Using add methods in the constructor, you can link multiple collections together. ```js -import firestorm from "firestorm-db"; -firestorm.address("ADDRESS_VALUE"); - const orders = firestorm.collection("orders"); // using the example of a customer having orders @@ -378,7 +405,7 @@ const customers = firestorm.collection("customers", (el) => { { field: "customer", criteria: "==", - // assuming the customers field in the orders collection stores by user ID + // assuming the customers field in the orders collection is a user ID value: el[firestorm.ID_FIELD] } ]) @@ -412,7 +439,7 @@ The first keys in a Firestorm request will always be the same regardless of its 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 warning +## Memory management Handling very large collections can cause memory allocation issues: 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/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index e864bf1..046841a 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -133,7 +133,7 @@ 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'); + throw new Exception('Incorrect keys type'); // else set it at the corresponding value $obj = $this->read(true); @@ -210,11 +210,11 @@ public function addBulk($values) { if ($this->autoKey == false) throw new Exception('Autokey disabled'); - // veriify that values is an array with number indices + // verify 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 + // else set it at the corresponding value $obj = $this->read(true); // decode and add all values diff --git a/src/index.js b/src/index.js index 53792f9..5fef89d 100644 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,7 @@ try { */ /** - * @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 @@ -20,7 +20,7 @@ try { */ /** - * @typedef {Object} ValueObject + * @typedef {Object} ValueOption * @property {string} field - Field to search * @property {boolean} [flatten] - Flatten array fields? (default false) */ @@ -83,8 +83,8 @@ const __extract_data = (request) => { }; /** - * Class representing a collection - * @template T + * Represents a Firestorm Collection +* @template T */ class Collection { /** @@ -201,37 +201,57 @@ class Collection { } /** - * Get an element from the collection - * @param {string | number} id - The ID of the element you want to get - * @returns {Promise} Corresponding value + * 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() { + // 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(id) { + get(key) { return this.__get_request("get", { - id: id, + id: key, }).then((res) => this.__add_methods(res)); } /** - * Get the sha1 hash of the file - * - Can be used to see if same file content without downloading the file - * @returns {string} The sha1 hash of the file + * Get multiple elements from the collection by their keys + * @param {string[] | number[]} keys - Array of keys to search + * @returns {Promise} The found elements */ - sha1() { - // string value is correct so we don't need validation - return this.__get_request("sha1", {}, false); + 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)) + search(options, random = false) { + if (!Array.isArray(options)) return Promise.reject(new TypeError("searchOptions shall be an array")); - searchOptions.forEach((searchOption) => { + options.forEach((searchOption) => { if ( searchOption.field === undefined || searchOption.criteria === undefined || @@ -251,7 +271,7 @@ class Collection { }); const params = { - search: searchOptions, + search: options, }; if (random !== false) { @@ -278,27 +298,8 @@ class Collection { } /** - * 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(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); - }); - } - - /** - * 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} * @returns {Promise>} The entire collection */ readRaw() { @@ -314,7 +315,8 @@ class Collection { } /** - * 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 */ @@ -324,14 +326,14 @@ 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 = {}; + select(option) { + if (!option) option = {}; return this.__get_request("select", { - select: selectOption, + select: option, }).then((data) => { Object.keys(data).forEach((key) => { data[key][ID_FIELD_NAME] = key; @@ -343,18 +345,18 @@ class Collection { /** * 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(new TypeError("Value option must be provided")); - if (typeof valueOption.field !== "string") + 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 (valueOption.flatten !== undefined && typeof valueOption.flatten !== "boolean") + if (option.flatten !== undefined && typeof option.flatten !== "boolean") return Promise.reject(new TypeError("Flatten must be a boolean")); return this.__get_request("values", { - values: valueOption, + values: option, }).then((data) => // no ID_FIELD or method injection since no ids are returned Object.values(data).filter((d) => d !== null), @@ -362,7 +364,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 @@ -406,7 +408,8 @@ class Collection { } /** - * 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 */ @@ -417,7 +420,8 @@ class Collection { } /** - * 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 @@ -427,9 +431,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 @@ -443,9 +448,10 @@ class Collection { } /** - * 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( @@ -454,7 +460,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 */ @@ -463,7 +469,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 */ @@ -472,8 +478,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 */ @@ -484,8 +490,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 */ @@ -496,22 +502,22 @@ class Collection { } /** - * Edit one field of the collection - * @param {EditObject} obj - The edit object + * Edit an element's field in the collection + * @param {EditFieldOption} option - The edit object * @returns {Promise<{ success: boolean }>} 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 + * Edit multiple elements' fields in the collection + * @param {EditFieldOption[]} options - The edit objects * @returns {Promise<{ success: boolean[] }>} 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)); } } @@ -550,7 +556,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,7 +567,7 @@ const firestorm = { * @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); diff --git a/typings/index.d.ts b/typings/index.d.ts index b451999..ad17fe0 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -93,7 +93,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 & ( | { @@ -194,22 +194,29 @@ declare 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 */ @@ -219,20 +226,15 @@ declare class Collection { ): 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 + * - ID values are injected for easier iteration, so this may be different from {@link sha1} * @returns The entire collection */ public readRaw(): 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 */ @@ -240,8 +242,8 @@ declare 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>( @@ -258,7 +260,7 @@ declare 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 @@ -267,14 +269,16 @@ declare 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 @@ -282,65 +286,67 @@ declare 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<{ success: boolean }>; /** - * 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<{ success: boolean[] }>; } -/** Value for the id field when searching content */ +/** Value for the ID field when searching content */ export const ID_FIELD: string; /** @@ -361,7 +367,7 @@ 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; @@ -369,7 +375,7 @@ export function collection(value: string, addMethods?: CollectionMethods): * 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): Collection; From 298f2b3121e627011efe97daf5e40e59f725c2f5 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sun, 21 Apr 2024 17:29:16 -0700 Subject: [PATCH 16/30] move around a few sections --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 68236dc..3bb8b49 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ firestorm.token("my_secret_token_probably_from_an_env_file"); Now you can use Firestorm to its full potential. -## Create your first Collection +## Create your first collection Firestorm is based around the concept of a `Collection`, which is akin to an SQL table or Firestore document. A Firestorm collection takes one required argument and one optional argument in its constructor: @@ -183,7 +183,7 @@ $database_list[$tmp->fileName] = $tmp; - `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 will be called in JavaScript in the Collection constructor (this can be different from the JSON filename if needed). -If you're working with multiple collections, it's easier to initialize them all in the array constructor directly: +If you're working with multiple collections, it's probably easier to initialize them all in the array constructor directly: ```php // config.php @@ -195,6 +195,14 @@ $database_list = array( ) ``` +## Permissions + +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: + +```sh +sudo chown -R www-data "/path/to/firestorm/root/" +``` + # Firestorm Files Firestorm's file APIs are implemented in `files.php`. If you don't need file-related features, then simply delete this file. @@ -210,13 +218,6 @@ $authorized_file_extension = array('.txt', '.png', '.jpg', '.jpeg'); $STORAGE_LOCATION = dirname($_SERVER['SCRIPT_FILENAME']) . '/uploads/'; ``` -Additionally, since the PHP scripts create folders and files, the script will fail if the PHP user doesn't have write permissions. -You can give Firestorm rights to a folder with the following command: - -```sh -sudo chown -R www-data "/path/to/uploads/" -``` - From there, you can use the functions in `firestorm.files` (detailed below) from the JavaScript client. ## Upload a file @@ -392,7 +393,7 @@ userCollection.set(123, { name: "John Doe", age: 30 }) .then((id) => userCollection.get(id)); ``` -## Combining Collections +## Combining collections Using add methods in the constructor, you can link multiple collections together. From b702ed2b4e4923dbe137d5ca6853459161eed626 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sun, 21 Apr 2024 19:02:44 -0700 Subject: [PATCH 17/30] refactor php to be cleaner and more readable mostly ternary hell, ridiculous indentation, and string concatenation --- README.md | 2 +- php/classes/FileAccess.php | 24 +- php/classes/HTTPException.php | 4 +- php/classes/JSONDatabase.php | 372 ++++++++++++++----------------- php/classes/read/random.php | 18 +- php/classes/read/searchArray.php | 2 +- php/config.php | 8 +- php/files.php | 20 +- php/get.php | 27 ++- php/post.php | 38 ++-- php/utils.php | 69 +++--- tests/config.php | 4 +- 12 files changed, 279 insertions(+), 309 deletions(-) diff --git a/README.md b/README.md index 3bb8b49..65e0776 100644 --- a/README.md +++ b/README.md @@ -381,7 +381,7 @@ $database_list['users'] = new JSONDatabase('users', false); ```js const userCollection = firestorm.collection("users"); -// Error: Autokey disabled +// Error: Automatic key generation is disabled await userCollection.add({ name: "John Doe", age: 30 }); ``` diff --git a/php/classes/FileAccess.php b/php/classes/FileAccess.php index bd25e42..14dd683 100644 --- a/php/classes/FileAccess.php +++ b/php/classes/FileAccess.php @@ -2,26 +2,26 @@ class FileAccess { public static function read($filepath, $waitLock = false, $default = null) { - $fileobj = array('filepath' => $filepath, 'content' => ''); + $fileObj = array('filepath' => $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); + throw new Exception("Could not open file: $filepath"); // set default value - $fileobj['content'] = $default; + $fileObj['content'] = $default; } // if no file, puts default value inside if ($file === false) { - file_put_contents($fileobj['filepath'], $fileobj['content'], LOCK_EX); + file_put_contents($fileObj['filepath'], $fileObj['content'], LOCK_EX); $file = fopen($filepath, 'rb'); } - $fileobj['fd'] = $file; + $fileObj['fd'] = $file; // if want the lock, we wait for the shared lock if ($waitLock) { @@ -38,24 +38,24 @@ public static function read($filepath, $waitLock = false, $default = null) { $string .= fread($file, 8192); } - $fileobj['content'] = $string; + $fileObj['content'] = $string; // if no wait you can close the file if (!$waitLock) fclose($file); - return $fileobj; + return $fileObj; } - public static function write($fileobj) { + public static function write($fileObj) { // lock and close - flock($fileobj['fd'], LOCK_UN); - fclose($fileobj['fd']); + flock($fileObj['fd'], LOCK_UN); + fclose($fileObj['fd']); - if (!is_writable($fileobj['filepath'])) { + 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); + $ret = file_put_contents($fileObj['filepath'], $fileObj['content'], LOCK_EX); return $ret; } } \ No newline at end of file diff --git a/php/classes/HTTPException.php b/php/classes/HTTPException.php index 5fe4b62..867ecbe 100644 --- a/php/classes/HTTPException.php +++ b/php/classes/HTTPException.php @@ -6,11 +6,11 @@ public function __construct($message, $code = 400, Throwable $previous = null) { $type_message = gettype($message); if ($type_message != 'string') - throw new Exception("Incorrect message type for HTTPException constructor, expected string, got " . $type_message); + throw new Exception("Incorrect message type for HTTPException constructor, expected string, got $type_message"); $type_code = gettype($code); if ($type_code != 'integer') - throw new Exception("Incorrect code type for HTTPException constructor, expected string, got " . $type_code); + throw new Exception("Incorrect code type for HTTPException constructor, expected string, got $type_code"); // assign everything parent::__construct($message, $code, $previous); diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index 046841a..f71d7c2 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -3,8 +3,8 @@ require_once('./utils.php'); require_once('./classes/FileAccess.php'); require_once('./classes/HTTPException.php'); -require_once(__DIR__.'/read/random.php'); -require_once(__DIR__.'/read/searchArray.php'); +require_once('./classes/read/random.php'); +require_once('./classes/read/searchArray.php'); class JSONDatabase { public $folderPath = './files/'; @@ -33,18 +33,16 @@ public function write_raw($content) { // content must not be primitive if (in_array($content_type, $incorrect_types)) { - throw new HTTPException('write_raw value cannot be a ' . $content_type, 400); - return 400; + 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) { + 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); - return 400; + throw new HTTPException("write_raw item cannot be a $item_type", 400); } } } @@ -58,8 +56,7 @@ public function write_raw($content) { // 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 cannot be a ' . $item_type, 400); - return 400; + throw new HTTPException("write_raw item with key $key item cannot be a $item_type", 400); } // we accept associative array as items because they may have an integer key @@ -91,7 +88,6 @@ public function read_raw($waitLock = false) { public function read($waitLock = false) { $res = $this->read_raw($waitLock); $res['content'] = json_decode($res['content'], true); - return $res; } @@ -107,7 +103,7 @@ public function get($key) { public function set($key, $value) { // "===" fixes the empty array "==" comparison if ($key === null or $value === null) { - throw new HTTPException("Key or value are null", 400); + throw new HTTPException('Key or value is null', 400); } $key_var_type = gettype($key); @@ -116,7 +112,7 @@ public function set($key, $value) { $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); + 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); @@ -142,7 +138,6 @@ public function setBulk($keys, $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'); @@ -157,27 +152,25 @@ public function setBulk($keys, $values) { private function newLastKey($arr) { if ($this->autoIncrement) { - $int_keys = array_filter(array_keys($arr), "is_int"); + $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(); - } + while (array_key_exists($last_key, $arr)) $last_key = uniqid(); } return strval($last_key); } public function add($value) { + if ($this->autoKey == false) + throw new Exception('Automatic key generation is disabled'); + // 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'); + 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); @@ -191,25 +184,26 @@ public function add($value) { } public function addBulk($values) { + if (!$this->autoKey) + throw new Exception('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 ($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); + 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) { + 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 (is_primitive($value) 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'); - // verify that values is an array with number indices if (array_assoc($values)) throw new Exception('Wanted sequential array'); @@ -220,7 +214,7 @@ public function addBulk($values) { // decode and add all values $values_decoded = $values; $id_array = array(); - foreach($values_decoded as $value_decoded) { + foreach ($values_decoded as $value_decoded) { $id = $this->newLastKey($obj['content']); $obj['content'][$id] = $value_decoded; @@ -235,7 +229,7 @@ public function addBulk($values) { public function remove($key) { if (gettype($key) != 'string') - throw new HTTPException("remove value must be a string", 400); + throw new HTTPException('remove value must be a string', 400); $obj = $this->read(true); unset($obj['content'][$key]); @@ -260,200 +254,165 @@ public function removeBulk($keys) { $obj = $this->read(true); // remove all keys - foreach($keys as $key_decoded) { + foreach ($keys as $key_decoded) { unset($obj['content'][$key_decoded]); } $this->write($obj); } + private function __search($concernedField, $criteria, $value, $ignoreCase) { + $fieldType = gettype($concernedField); + switch ($fieldType) { + case 'boolean': + switch ($criteria) { + case '!=': + return $concernedField != $value; + case '==': + return $concernedField == $value; + default: + return false; + } + case 'integer': + case 'double': + switch ($criteria) { + case '!=': + return $concernedField != $value; + case '==': + return $concernedField == $value; + case '>=': + return $concernedField >= $value; + case '<=': + return $concernedField <= $value; + case '<': + return $concernedField < $value; + case '>': + return $concernedField > $value; + case 'in': + return in_array($concernedField, $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($concernedField, $value) != 0; + case '==': + return $cmpFunc($concernedField, $value) == 0; + case '>=': + return $cmpFunc($concernedField, $value) >= 0; + case '<=': + return $cmpFunc($concernedField, $value) <= 0; + case '<': + return $cmpFunc($concernedField, $value) < 0; + case '>': + return $cmpFunc($concernedField, $value) > 0; + case 'includes': + case 'contains': + return $value != '' ? ($posFunc($concernedField, $value) !== false) : true; + case 'startsWith': + return $value != '' ? ($posFunc($concernedField, $value) === 0) : true; + case 'endsWith': + $end = substr($concernedField, -strlen($value)); + return $value != '' ? ($cmpFunc($end, $value) === 0) : true; + case 'in': + $notFound = true; + $a_i = 0; + while ($a_i < count($value) && $notFound) { + $notFound = $cmpFunc($concernedField, $value[$a_i]) != 0; + $a_i++; + } + return !$notFound; + default: + return false; + } + case 'array': + switch ($criteria) { + case 'array-contains': + return array_contains($concernedField, $value, $ignoreCase); + case 'array-contains-any': + return array_contains_any($concernedField, $value, $ignoreCase); + case 'array-length': + case 'array-length-eq': + return count($concernedField) == $value; + case 'array-length-df': + return count($concernedField) != $value; + case 'array-length-gt': + return count($concernedField) > $value; + case 'array-length-lt': + return count($concernedField) < $value; + case 'array-length-ge': + return count($concernedField) >= $value; + case 'array-length-le': + return count($concernedField) <= $value; + default: + return false; + } + default: + break; + } + } + public function search($conditions, $random = false) { $obj = $this->read(); $res = []; - foreach(array_keys($obj['content']) as $key) { + 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]; + foreach ($conditions as $condition) { + // cleaner than a million breaks inside the switch statement + if (!$add) break; // extract field $field = $condition['field']; - $field_path = explode(".", $field); - - $field_ind = 0; + $field_path = explode('.', $field); - while($el != NULL && $field_ind + 1 < count($field_path)) { + for ($field_ind = 0; $el != NULL && $field_ind + 1 < count($field_path); $field_ind += 1) { // 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_ind += 1; - $field = $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)) { - $criteria = $condition['criteria']; - $value = $condition['value']; + if ($el == NULL || + !array_key_exists($field, $el) || + !array_key_exists('criteria', $condition) || + !array_key_exists('value', $condition) + ) { + $add = false; + break; + } - // get field to compare - $concernedField = $el[$field]; + $criteria = $condition['criteria']; + $value = $condition['value']; - // get concerned field type - $fieldType = gettype($concernedField); + // get field to compare + $concernedField = $el[$field]; - if ($criteria == 'array-contains' || $criteria == 'array-contains-any') { - $ignoreCase = array_key_exists('ignoreCase', $condition) && !!$condition['ignoreCase']; - } + $ignoreCase = array_key_exists('ignoreCase', $condition) && !!$condition['ignoreCase']; + $add = $this->__search($concernedField, $criteria, $value, $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 ($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"); + if (!is_int($rawSeed)) + throw new HTTPException('Seed not an integer value for random search result'); $seed = intval($rawSeed); } $res = chooseRandom($res, $seed); @@ -469,7 +428,7 @@ public function searchKeys($searchedKeys) { if (gettype($searchedKeys) != 'array') return $res; - foreach($searchedKeys as $key) { + foreach ($searchedKeys as $key) { $key = strval($key); if (array_key_exists($key, $obj['content'])) { @@ -480,10 +439,6 @@ public function searchKeys($searchedKeys) { 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)) @@ -532,7 +487,7 @@ private function __edit(&$obj, $editObj) { if (!isset($obj['content'][$id][$field]) and ($operation != 'set' and $operation != 'remove' and $operation != 'array-push')) return false; - switch($operation) { + switch ($operation) { case 'set': $obj['content'][$id][$field] = $value; return true; @@ -625,16 +580,19 @@ private function __edit(&$obj, $editObj) { return false; } + public function editField($editObj) { + return $this->editFieldBulk(array($editObj))[0]; + } + public function editFieldBulk($objArray) { // need sequential array - if (array_assoc($objArray)) - return false; + if (array_assoc($objArray)) return false; $arrayResult = array(); $fileObj = $this->read($this); - foreach($objArray as &$editObj) { + foreach ($objArray as &$editObj) { array_push($arrayResult, $this->__edit($fileObj, $editObj)); } @@ -644,21 +602,17 @@ public function editFieldBulk($objArray) { } 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'); + if (!array_key_exists('fields', $selectObj)) 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'); + 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']; - $i = 0; $fields_count = count($fields); - while($i < $fields_count && $verif_fields) { - $verif_fields = gettype($fields[$i]) === 'string'; - ++$i; + foreach ($fields as $field) { + if (gettype($field) !== 'string') + throw new HTTPException('fields field incorrect, expected a string array'); } - if (!$verif_fields) throw new HTTPException('fields field incorrect, expected an array of string'); $obj = $this->read(); @@ -695,7 +649,7 @@ public function values($valueObj) { $json = $obj['content']; $result = []; - foreach ($json as $key => $value) { + foreach ($json as $value) { // get correct field and skip existing primitive values (faster) if (!array_key_exists($field, $value) || in_array($value, $result)) continue; @@ -715,5 +669,3 @@ public function random($params) { return random($params, $this); } } - -?> \ No newline at end of file diff --git a/php/classes/read/random.php b/php/classes/read/random.php index 60e99ce..9cdd511 100644 --- a/php/classes/read/random.php +++ b/php/classes/read/random.php @@ -1,13 +1,15 @@ = 0 for the offset'); + if ( + $hasOffset && + (gettype($offset) !== 'integer' || + $offset < 0) + ) throw new HTTPException('Expected integer >= 0 for the offset'); - // seed verif + // seed validation $seed = $hasSeed ? $params['seed'] : false; if ($hasSeed && gettype($seed) !== 'integer') throw new HTTPException('Expected integer for the seed'); @@ -57,7 +63,7 @@ function chooseRandom($json, $seed = false, $max = -1, $offset = 0) { // -> I still have keys // -> I am not at maximum $i = 0; - while($keys_length > 0 && $i < $max) { + while ($keys_length > 0 && $i < $max) { // get an index $index = mt_rand(0, $keys_length - 1); diff --git a/php/classes/read/searchArray.php b/php/classes/read/searchArray.php index e26b724..97a6451 100644 --- a/php/classes/read/searchArray.php +++ b/php/classes/read/searchArray.php @@ -17,7 +17,7 @@ function array_contains($array, $value, $ignoreCase = false) { function array_contains_any($concernedField, $value, $ignoreCase = false) { $add = false; - if (gettype($value) === "array") { + if (gettype($value) === 'array') { $tmp = false; $val_i = 0; while ($val_i < count($value) and !$tmp) { diff --git a/php/config.php b/php/config.php index 8154b9e..7f013ec 100644 --- a/php/config.php +++ b/php/config.php @@ -3,7 +3,7 @@ require_once('./utils.php'); require_once('./classes/JSONDatabase.php'); -$log_path = "firestorm.log"; +$log_path = 'firestorm.log'; $database_list = array(); /** @@ -15,7 +15,7 @@ $db = new JSONDatabase; // This will be the name of the JSON file // It has to be the same as the actual file name (no extension) -$db->fileName = "my_json_name"; +$db->fileName = 'my_json_name'; // Whether to automatically generate the key name or to have explicit key names // - Default: true $db->autoKey = true; @@ -24,12 +24,12 @@ // - Default: true $db->autoIncrement = true; // The database_list key is what the collection will be called in JavaScript -$database_list["my_collection_name"] = $db; +$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); +$database_list['my_collection_name'] = new JSONDatabase('my_json_name', true, true); /** * File handling: diff --git a/php/files.php b/php/files.php index f4088fe..8be98d4 100644 --- a/php/files.php +++ b/php/files.php @@ -15,7 +15,7 @@ $method = sec($_SERVER['REQUEST_METHOD']); if ($method !== 'GET' && $method !== 'POST' && $method !== 'DELETE') { - http_error(400, 'Incorrect request type, expected GET, POST or DELETE, not ' . $method); + http_error(400, "Incorrect request type, expected GET, POST or DELETE, not $method"); } @@ -57,7 +57,7 @@ $extensionFound = false; $i = 0; - while($i < count($authorized_file_extension) && !$extensionFound) { + while ($i < count($authorized_file_extension) && !$extensionFound) { $extensionFound = str_ends_with($absolutePath, $authorized_file_extension[$i]); $i = $i + 1; } @@ -75,7 +75,7 @@ if (!$overwrite && file_exists($absolutePath)) http_error(403, 'File already exists'); - $uploaddir = dirname($absolutePath); + $uploadDir = dirname($absolutePath); // Make sure you can write to this folder. // php default user is www-data @@ -83,8 +83,8 @@ // sudo chown -R www-data "/path/to/folder/" // mkdir(path, rw-r--r--, recursive=true) - if (!is_dir($uploaddir) && !mkdir($uploaddir, 0766, true)){ - http_error(500, "PHP script can't create folder " . $uploaddir . ", check permission, group and owner."); + if (!is_dir($uploadDir) && !mkdir($uploadDir, 0766, true)) { + http_error(500, "PHP script can't create folder " . $uploadDir . ". Check permission, group and owner."); } if (!check($_FILES) || !check($_FILES['file'])) http_error(400, 'No actual file was given'); @@ -93,9 +93,9 @@ // eventually write the file if (move_uploaded_file($tmpName, $absolutePath)) { - http_success('Written file successfully to ' . $relativePath); + http_success("Written file successfully to $relativePath"); } else { - http_error(500, "PHP script can't write to file, check permission, group and owner."); + http_error(500, "PHP script can't write to file. Check permission, group and owner."); } die(); @@ -115,8 +115,8 @@ try { // try to read the image - $imginfo = getimagesize($absolutePath); - header("Content-type: {$imginfo['mime']}"); + $imgInfo = getimagesize($absolutePath); + header("Content-type: {$imgInfo['mime']}"); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Pragma: public'); @@ -128,7 +128,7 @@ } catch (Throwable $th) { header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); - header('Content-Disposition: attachment; filename='.basename($absolutePath)); + header('Content-Disposition: attachment; filename=' . basename($absolutePath)); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); diff --git a/php/get.php b/php/get.php index ce13faf..1fed0f4 100644 --- a/php/get.php +++ b/php/get.php @@ -13,7 +13,7 @@ $method = sec($_SERVER['REQUEST_METHOD']); if ($method !== 'GET' && $method !== 'POST') { - http_error(400, 'Incorrect request type, expected GET or POST, not ' . $method); + http_error(400, "Incorrect request type, expected GET or POST, not $method"); } $inputJSON = json_decode(file_get_contents('php://input'), true); @@ -35,7 +35,7 @@ // checking good collection if (!array_key_exists($collection, $database_list)) - http_error(404, 'Collection not found: ' . $collection); + http_error(404, "Collection not found: $collection"); /** * @var JSONDatabase @@ -46,12 +46,21 @@ if (!$command) http_error(400, 'No command provided'); -$commands_available = ['read_raw', 'get', 'search', 'searchKeys', 'select', 'random', 'sha1', 'values']; - -if (!in_array($command, $commands_available)) - http_error(404, 'Command not found: ' . $command . '. Available commands: ' . join(', ', $commands_available)); - -switch($command) { +$available_commands = [ + 'read_raw', + 'get', + 'search', + 'searchKeys', + 'select', + 'random', + 'sha1', + 'values' +]; + +if (!in_array($command, $available_commands)) + http_error(404, "Command not found: $command. Available commands: " . join(', ', $available_commands)); + +switch ($command) { case 'sha1': $res = $db->sha1(); http_response($res); @@ -70,7 +79,7 @@ $result = $db->get($id); if (!$result) - http_error(404, 'get failed on collection ' . $collection . ' with key ' . $id); + http_error(404, "get failed on collection $collection with key $id"); http_response(stringifier($result)); break; diff --git a/php/post.php b/php/post.php index ec0e6bd..bcd3647 100644 --- a/php/post.php +++ b/php/post.php @@ -8,7 +8,7 @@ $method = sec($_SERVER['REQUEST_METHOD']); if ($method === 'GET') { - http_error(400, 'Incorrect request type, expected POST, not ' . $method); + http_error(400, "Incorrect request type, expected POST, not $method"); } $inputJSON = json_decode(file_get_contents('php://input'), true); @@ -48,7 +48,7 @@ // checking good collection if (!array_key_exists($collection, $database_list)) - http_error(404, 'Collection not found: ' . $collection); + http_error(404, "Collection not found: $collection"); $db = $database_list[$collection]; @@ -56,21 +56,31 @@ if ($command === false) http_error(400, 'No command provided'); -$commands_available = ['write_raw', 'add', 'addBulk', 'remove', 'removeBulk', 'set', 'setBulk', 'editField', 'editFieldBulk']; - -if (!in_array($command, $commands_available)) - http_error(404, 'Command not found: ' . $command . '. Available commands: ' . join(', ', $commands_available)); +$available_commands = [ + 'write_raw', + 'add', + 'addBulk', + 'remove', + 'removeBulk', + 'set', + 'setBulk', + 'editField', + 'editFieldBulk' +]; + +if (!in_array($command, $available_commands)) + http_error(404, "Command not found: $command. Available commands: " . join(', ', $available_commands)); $valueKeyName = ($command != 'setBulk' && $command != 'addBulk') ? 'value' : 'values'; $value = check_key_json($valueKeyName, $inputJSON, false); if ($value === false) - http_error(400, 'No ' . $valueKeyName . ' provided'); + http_error(400, "No $valueKeyName provided"); -switch($command) { +switch ($command) { case 'write_raw': $db->write_raw($value); - http_success('Successful ' . $command . ' command'); + http_success("Successful $command command"); break; case 'add': $newId = $db->add($value); @@ -82,11 +92,11 @@ 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); @@ -94,7 +104,7 @@ 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); @@ -102,7 +112,7 @@ 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); @@ -122,7 +132,7 @@ 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()); diff --git a/php/utils.php b/php/utils.php index 7edbbef..5d2764e 100644 --- a/php/utils.php +++ b/php/utils.php @@ -7,10 +7,7 @@ function pre_dump($val) { } function check($var) { - if (isset($var) and !empty($var)) { - return true; - } - return false; + return isset($var) and !empty($var); } function sec($var) { @@ -20,7 +17,6 @@ function sec($var) { function http_response($body, $code=200) { header('Content-Type: application/json'); http_response_code($code); - echo $body; exit(); @@ -39,6 +35,15 @@ 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 http_success($message) { http_message($message, 'message', 200); } @@ -57,14 +62,12 @@ function array_sequential(array $arr) { } function stringifier($obj, $depth = 1) { - if ($depth == 0) { - return json_encode($obj); - } + if ($depth == 0) return json_encode($obj); $res = "{"; $formed = array(); - foreach(array_keys($obj) as $key) { + foreach (array_keys($obj) as $key) { array_push($formed, '"' . strval($key) . '":' . stringifier($obj[$key], $depth - 1)); } $res .= implode(",", $formed); @@ -75,24 +78,24 @@ function stringifier($obj, $depth = 1) { } 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 -} + // 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') { + // 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_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']}"); + if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) + header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}"); - exit(0); -} + exit(0); + } } function removeDots($path) { @@ -100,25 +103,17 @@ function removeDots($path) { $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); - } + 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 - { +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/tests/config.php b/tests/config.php index b12b89b..1375934 100644 --- a/tests/config.php +++ b/tests/config.php @@ -23,6 +23,4 @@ $database_list[$tmp->fileName] = $tmp; -$log_path = "firestorm.log"; - -?> \ No newline at end of file +$log_path = 'firestorm.log'; From 1fa5907f755e5e778167c7ef58a4d367493629e6 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sun, 21 Apr 2024 19:26:02 -0700 Subject: [PATCH 18/30] add more tests to collection.values() --- tests/js-test.spec.js | 76 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 639510c..7a02161 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -513,7 +513,7 @@ describe("GET operations", () => { // all incorrect values must catch const incorrect = [undefined, null, false, 5, 12.5, "gg", { toto: "tata" }]; incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} value`, (done) => { + it(`${JSON.stringify(unco)} value rejects`, (done) => { base .select({ fields: unco }) .then((res) => done(`got ${JSON.stringify(res)} value`)) @@ -537,7 +537,7 @@ describe("GET operations", () => { // incorrect arrays incorrect = [undefined, null, false, 5, 12.5, {}]; incorrect.forEach(async (unco) => { - it(`[${JSON.stringify(unco)}] value`, (done) => { + it(`[${JSON.stringify(unco)}] value rejects`, (done) => { base .select({ fields: [unco] }) .then(() => done(`[${JSON.stringify(unco)}] value passed`)) @@ -591,6 +591,76 @@ describe("GET operations", () => { .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((unco) => { + it(`${JSON.stringify(unco)} value rejects`, (done) => { + base + .values({ field: unco }) + .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((err) => done(err)); + }); + + 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((err) => done(err)); + }); + + 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((err) => done(err)); + }); + + 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((err) => done(err)); + }); + }); }); describe("random(max, seed, offset)", () => { @@ -609,7 +679,7 @@ describe("GET operations", () => { 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 + const 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) => { base From e07e526af510fa881f2382a9447c305359dab16d Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sun, 21 Apr 2024 23:04:37 -0700 Subject: [PATCH 19/30] fix posix end of file stuff --- php/classes/FileAccess.php | 2 +- php/classes/HTTPException.php | 4 ++-- php/classes/read/random.php | 2 +- php/config.php | 2 +- php/files.php | 2 +- php/log.php | 2 +- php/post.php | 2 +- php/tokens.php | 2 +- src/index.js | 2 +- typings/index.d.ts | 4 +++- 10 files changed, 13 insertions(+), 11 deletions(-) diff --git a/php/classes/FileAccess.php b/php/classes/FileAccess.php index 14dd683..f0fc7fb 100644 --- a/php/classes/FileAccess.php +++ b/php/classes/FileAccess.php @@ -58,4 +58,4 @@ public static function write($fileObj) { $ret = file_put_contents($fileObj['filepath'], $fileObj['content'], LOCK_EX); return $ret; } -} \ No newline at end of file +} diff --git a/php/classes/HTTPException.php b/php/classes/HTTPException.php index 867ecbe..68f3a57 100644 --- a/php/classes/HTTPException.php +++ b/php/classes/HTTPException.php @@ -17,7 +17,7 @@ public function __construct($message, $code = 400, Throwable $previous = null) { } // prettier representation - public function __toString() { + public function __toString(): string { return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; } -} \ No newline at end of file +} diff --git a/php/classes/read/random.php b/php/classes/read/random.php index 9cdd511..a0e7df6 100644 --- a/php/classes/read/random.php +++ b/php/classes/read/random.php @@ -85,4 +85,4 @@ function chooseRandom($json, $seed = false, $max = -1, $offset = 0) { } return $result; -} \ No newline at end of file +} diff --git a/php/config.php b/php/config.php index 7f013ec..11410cd 100644 --- a/php/config.php +++ b/php/config.php @@ -41,4 +41,4 @@ // 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/'; \ No newline at end of file +$STORAGE_LOCATION = dirname($_SERVER['SCRIPT_FILENAME']) . '/uploads/'; diff --git a/php/files.php b/php/files.php index 8be98d4..c133ced 100644 --- a/php/files.php +++ b/php/files.php @@ -205,4 +205,4 @@ function g($var) { } return sec($_GET[$var]); -} \ No newline at end of file +} diff --git a/php/log.php b/php/log.php index 8b31c0a..a24ecfc 100644 --- a/php/log.php +++ b/php/log.php @@ -13,4 +13,4 @@ public static function addLog($message) { fwrite($fp, '\n'); fclose($fp); } -} \ No newline at end of file +} diff --git a/php/post.php b/php/post.php index bcd3647..2ce2179 100644 --- a/php/post.php +++ b/php/post.php @@ -138,4 +138,4 @@ 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 index 0af90ff..74955ec 100644 --- a/php/tokens.php +++ b/php/tokens.php @@ -1,3 +1,3 @@ { /** * Represents a Firestorm Collection -* @template T + * @template T */ class Collection { /** diff --git a/typings/index.d.ts b/typings/index.d.ts index ad17fe0..601cf3f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -343,7 +343,9 @@ declare class Collection { * @param options - The edit objects * @returns Edit confirmation */ - public editFieldBulk(options: EditFieldOption>[]): Promise<{ success: boolean[] }>; + public editFieldBulk( + options: EditFieldOption>[], + ): Promise<{ success: boolean[] }>; } /** Value for the ID field when searching content */ From 38af96bdb906558909822d05dfe48405e0125b72 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Tue, 23 Apr 2024 16:59:28 -0700 Subject: [PATCH 20/30] rename concernedField to field --- php/classes/JSONDatabase.php | 62 ++++++++++++++++++------------------ tests/js-test.spec.js | 26 +++++++-------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index f71d7c2..dde3e3e 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -261,15 +261,15 @@ public function removeBulk($keys) { $this->write($obj); } - private function __search($concernedField, $criteria, $value, $ignoreCase) { - $fieldType = gettype($concernedField); + private function __search($field, $criteria, $value, $ignoreCase) { + $fieldType = gettype($field); switch ($fieldType) { case 'boolean': switch ($criteria) { case '!=': - return $concernedField != $value; + return $field != $value; case '==': - return $concernedField == $value; + return $field == $value; default: return false; } @@ -277,19 +277,19 @@ private function __search($concernedField, $criteria, $value, $ignoreCase) { case 'double': switch ($criteria) { case '!=': - return $concernedField != $value; + return $field != $value; case '==': - return $concernedField == $value; + return $field == $value; case '>=': - return $concernedField >= $value; + return $field >= $value; case '<=': - return $concernedField <= $value; + return $field <= $value; case '<': - return $concernedField < $value; + return $field < $value; case '>': - return $concernedField > $value; + return $field > $value; case 'in': - return in_array($concernedField, $value); + return in_array($field, $value); default: return false; } @@ -299,30 +299,30 @@ private function __search($concernedField, $criteria, $value, $ignoreCase) { $posFunc = $ignoreCase ? 'stripos' : 'strpos'; switch ($criteria) { case '!=': - return $cmpFunc($concernedField, $value) != 0; + return $cmpFunc($field, $value) != 0; case '==': - return $cmpFunc($concernedField, $value) == 0; + return $cmpFunc($field, $value) == 0; case '>=': - return $cmpFunc($concernedField, $value) >= 0; + return $cmpFunc($field, $value) >= 0; case '<=': - return $cmpFunc($concernedField, $value) <= 0; + return $cmpFunc($field, $value) <= 0; case '<': - return $cmpFunc($concernedField, $value) < 0; + return $cmpFunc($field, $value) < 0; case '>': - return $cmpFunc($concernedField, $value) > 0; + return $cmpFunc($field, $value) > 0; case 'includes': case 'contains': - return $value != '' ? ($posFunc($concernedField, $value) !== false) : true; + return $value != '' ? ($posFunc($field, $value) !== false) : true; case 'startsWith': - return $value != '' ? ($posFunc($concernedField, $value) === 0) : true; + return $value != '' ? ($posFunc($field, $value) === 0) : true; case 'endsWith': - $end = substr($concernedField, -strlen($value)); + $end = substr($field, -strlen($value)); return $value != '' ? ($cmpFunc($end, $value) === 0) : true; case 'in': $notFound = true; $a_i = 0; while ($a_i < count($value) && $notFound) { - $notFound = $cmpFunc($concernedField, $value[$a_i]) != 0; + $notFound = $cmpFunc($field, $value[$a_i]) != 0; $a_i++; } return !$notFound; @@ -332,22 +332,22 @@ private function __search($concernedField, $criteria, $value, $ignoreCase) { case 'array': switch ($criteria) { case 'array-contains': - return array_contains($concernedField, $value, $ignoreCase); + return array_contains($field, $value, $ignoreCase); case 'array-contains-any': - return array_contains_any($concernedField, $value, $ignoreCase); + return array_contains_any($field, $value, $ignoreCase); case 'array-length': case 'array-length-eq': - return count($concernedField) == $value; + return count($field) == $value; case 'array-length-df': - return count($concernedField) != $value; + return count($field) != $value; case 'array-length-gt': - return count($concernedField) > $value; + return count($field) > $value; case 'array-length-lt': - return count($concernedField) < $value; + return count($field) < $value; case 'array-length-ge': - return count($concernedField) >= $value; + return count($field) >= $value; case 'array-length-le': - return count($concernedField) <= $value; + return count($field) <= $value; default: return false; } @@ -396,10 +396,10 @@ public function search($conditions, $random = false) { $value = $condition['value']; // get field to compare - $concernedField = $el[$field]; + $fieldValue = $el[$field]; $ignoreCase = array_key_exists('ignoreCase', $condition) && !!$condition['ignoreCase']; - $add = $this->__search($concernedField, $criteria, $value, $ignoreCase); + $add = $this->__search($fieldValue, $criteria, $value, $ignoreCase); $el = $el_root; } diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 7a02161..9ec19ed 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -208,7 +208,7 @@ describe("GET operations", () => { expect(res).deep.equals(content, "Content different"); done(); }) - .catch((err) => done(err)); + .catch(done); }); it("sha1 content hash is the same", (done) => { @@ -235,7 +235,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) => { @@ -246,7 +246,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) => { @@ -256,7 +256,7 @@ describe("GET operations", () => { expect(results[0]).deep.equals(results[1], "Content different"); done(); }) - .catch((err) => done(err)); + .catch(done); }); }); @@ -398,7 +398,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 @@ -528,7 +528,7 @@ describe("GET operations", () => { base .select({ fields: val }) .then(() => done()) - .catch((err) => done(err)); + .catch(done); }); }); }); @@ -566,7 +566,7 @@ describe("GET operations", () => { expect(selectResult).to.be.deep.equal(raw, `contents are different`); done(); }) - .catch((err) => done(err)); + .catch(done); }); }); @@ -614,7 +614,7 @@ describe("GET operations", () => { expect(res.sort()).to.deep.equal(expected.sort()); done(); }) - .catch((err) => done(err)); + .catch(done); }); it("works with an array with flattening", (done) => { @@ -631,7 +631,7 @@ describe("GET operations", () => { expect(res.sort()).to.deep.equal(expected.sort()); done(); }) - .catch((err) => done(err)); + .catch(done); }); it("works on primitive with flattening", (done) => { @@ -643,7 +643,7 @@ describe("GET operations", () => { expect(res.sort()).to.deep.equal(expected.sort()); done(); }) - .catch((err) => done(err)); + .catch(done); }); it("works on an array without flattening", (done) => { @@ -658,7 +658,7 @@ describe("GET operations", () => { expect(res.sort()).to.deep.equal(unique.sort()); done(); }) - .catch((err) => done(err)); + .catch(done); }); }); }); @@ -836,7 +836,7 @@ describe("POST operations", () => { expect(id).to.equals(String(parseInt(last_id) + 1)); done(); }) - .catch((err) => done(err)); + .catch(done); }); describe("It should not accept incorrect values", () => { @@ -877,7 +877,7 @@ describe("POST operations", () => { base .add(co) .then(() => done()) - .catch((err) => done(err)); + .catch(done); }); }); }); From 9474723ce16acba7e17ed7127b152afb2125611a Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Tue, 23 Apr 2024 17:21:39 -0700 Subject: [PATCH 21/30] don't start a method with double underscores not future proof --- php/classes/JSONDatabase.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index dde3e3e..11d6614 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -261,7 +261,7 @@ public function removeBulk($keys) { $this->write($obj); } - private function __search($field, $criteria, $value, $ignoreCase) { + private function _search($field, $criteria, $value, $ignoreCase) { $fieldType = gettype($field); switch ($fieldType) { case 'boolean': @@ -392,14 +392,8 @@ public function search($conditions, $random = false) { break; } - $criteria = $condition['criteria']; - $value = $condition['value']; - - // get field to compare - $fieldValue = $el[$field]; - $ignoreCase = array_key_exists('ignoreCase', $condition) && !!$condition['ignoreCase']; - $add = $this->__search($fieldValue, $criteria, $value, $ignoreCase); + $add = $this->_search($el[$field], $condition['criteria'], $condition['value'], $ignoreCase); $el = $el_root; } @@ -440,7 +434,7 @@ public function searchKeys($searchedKeys) { } // MANDATORY REFERENCE to edit directly: PHP 5+ - private function __edit(&$obj, $editObj) { + private function _edit(&$obj, $editObj) { if (!is_object($editObj)) return false; @@ -593,7 +587,7 @@ public function editFieldBulk($objArray) { $fileObj = $this->read($this); foreach ($objArray as &$editObj) { - array_push($arrayResult, $this->__edit($fileObj, $editObj)); + array_push($arrayResult, $this->_edit($fileObj, $editObj)); } $this->write($fileObj); From 31a2fb83d179af6abc9a18108d884596595de948 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sun, 28 Apr 2024 10:38:16 -0700 Subject: [PATCH 22/30] run php formatter --- README.md | 4 ++-- php/classes/FileAccess.php | 2 +- php/classes/JSONDatabase.php | 26 +++++++++++++++----------- php/classes/read/random.php | 31 ++++++++++++++----------------- php/classes/read/searchArray.php | 2 +- php/config.php | 4 ++-- php/files.php | 32 ++++++++++++++++---------------- php/get.php | 8 ++++---- php/index.php | 2 +- php/post.php | 10 +++++----- php/utils.php | 16 +++++++--------- tests/config.php | 12 ++++++------ tests/tokens.php | 6 +----- 13 files changed, 75 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 65e0776..ca38884 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ The server-side files to handle requests can be found and copied to your hosting ```php new JSONDatabase('orders', true), 'users' => new JSONDatabase('users', false), diff --git a/php/classes/FileAccess.php b/php/classes/FileAccess.php index f0fc7fb..4e6b038 100644 --- a/php/classes/FileAccess.php +++ b/php/classes/FileAccess.php @@ -56,6 +56,6 @@ public static function write($fileObj) { } $ret = file_put_contents($fileObj['filepath'], $fileObj['content'], LOCK_EX); - return $ret; + return $ret; } } diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index 11d6614..d01cc10 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -1,10 +1,10 @@ 2) array_splice($obj['content'][$id][$field], $value[0], $value[1], $value[2]); - else array_splice($obj['content'][$id][$field], $value[0], $value[1]); + 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: diff --git a/php/classes/read/random.php b/php/classes/read/random.php index a0e7df6..be51653 100644 --- a/php/classes/read/random.php +++ b/php/classes/read/random.php @@ -1,6 +1,6 @@ = -1 for the max'); + if ($hasMax && (gettype($max) !== 'integer' || $max < -1)) + throw new HTTPException('Expected integer >= -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'); + 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'); + 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'); + if ($hasSeed && gettype($seed) !== 'integer') + throw new HTTPException('Expected integer for the seed'); $json = $class->read()['content']; @@ -43,7 +43,8 @@ function chooseRandom($json, $seed = false, $max = -1, $offset = 0) { $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 + // return an empty array, can't get more elements + if ($offset >= $keys_length) return array(); if ($max == -1 || $max > $keys_length) $max = $keys_length; @@ -62,8 +63,7 @@ function chooseRandom($json, $seed = false, $max = -1, $offset = 0) { // then while I can get new entries // -> I still have keys // -> I am not at maximum - $i = 0; - while ($keys_length > 0 && $i < $max) { + for ($i = 0; $keys_length > 0 && $i < $max; ++$i) { // get an index $index = mt_rand(0, $keys_length - 1); @@ -72,15 +72,12 @@ function chooseRandom($json, $seed = false, $max = -1, $offset = 0) { // 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]); + foreach ($keys_selected as $k) { + $key = strval($k); $result[$key] = $json[$key]; } diff --git a/php/classes/read/searchArray.php b/php/classes/read/searchArray.php index 97a6451..61fec0d 100644 --- a/php/classes/read/searchArray.php +++ b/php/classes/read/searchArray.php @@ -5,7 +5,7 @@ function array_contains($array, $value, $ignoreCase = false) { $tmp_i = 0; while ($tmp_i < count($array) and !$tmp) { if ($ignoreCase) { - $tmp = ($ignoreCase ? strcasecmp($array[$tmp_i], $value) : strcmp($array[$tmp_i], $value)) == 0; + $tmp = ($ignoreCase ? strcasecmp($array[$tmp_i], $value) : strcmp($array[$tmp_i], $value)) == 0; } else { $tmp = $array[$tmp_i] == $value; } diff --git a/php/config.php b/php/config.php index 11410cd..ca95e9e 100644 --- a/php/config.php +++ b/php/config.php @@ -1,7 +1,7 @@ new JSONDatabase('house', false) +); // test without constructor $tmp = new JSONDatabase; diff --git a/tests/tokens.php b/tests/tokens.php index 2413c9e..0966c63 100644 --- a/tests/tokens.php +++ b/tests/tokens.php @@ -1,7 +1,3 @@ 'NeverGonnaGiveYouUp' -); - -$db_tokens = array_values($db_tokens_map); \ No newline at end of file +$db_tokens = array("NeverGonnaGiveYouUp"); From e1ab419d81eed367f4c604bebd8f7e851d2b8bf2 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sun, 28 Apr 2024 11:39:27 -0700 Subject: [PATCH 23/30] add some utility methods, fix remove() error --- CHANGELOG.md | 1 + php/classes/JSONDatabase.php | 158 +++++++++++++++++++++-------------- php/classes/read/random.php | 4 +- php/utils.php | 11 ++- tests/js-test.spec.js | 141 ++++++++++++++----------------- 5 files changed, 173 insertions(+), 142 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 010452d..0d1d476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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. ## [1.12.0] - 2024-02-22 diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index d01cc10..a912fa3 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -7,16 +7,23 @@ require_once './classes/read/searchArray.php'; class JSONDatabase { + /** Folder to get the JSON file from */ public $folderPath = './files/'; + /** Name of the JSON file */ public $fileName = 'db'; + /** File extension used in collection name */ public $fileExt = '.json'; - public $default = array(); - + /** Whether to automatically generate the key name or to have explicit key names */ public $autoKey = true; + /** Whether to simply start at 0 and increment or to use a random ID name */ public $autoIncrement = true; - public function __construct(string $fileName = 'db', bool $autoKey = true, bool $autoIncrement = true) { + public function __construct( + string $fileName = 'db', + bool $autoKey = true, + bool $autoIncrement = true + ) { // if no/some args provided they just fall back to their defaults $this->fileName = $fileName; $this->autoKey = $autoKey; @@ -82,7 +89,8 @@ public function sha1() { } public function read_raw($waitLock = false) { - return FileAccess::read($this->fullPath(), $waitLock, json_encode($this->default)); + // fall back to empty array if failed + return FileAccess::read($this->fullPath(), $waitLock, json_encode(array())); } public function read($waitLock = false) { @@ -93,7 +101,11 @@ public function read($waitLock = false) { public function get($key) { $obj = $this->read(); - if (!$obj || array_key_exists('content', $obj) == false || array_key_exists(strval($key), $obj['content']) == false) + if ( + !$obj || + array_key_exists('content', $obj) == false || + array_key_exists(strval($key), $obj['content']) == false + ) return null; $res = array($key => $obj['content'][$key]); @@ -107,11 +119,11 @@ public function set($key, $value) { } $key_var_type = gettype($key); - if ($key_var_type != 'string' and $key_var_type != 'integer') - throw new HTTPException('Incorrect key', 400); + 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 ($value_var_type == 'double' or $value_var_type == 'integer' or $value_var_type == 'string') + if (is_primitive($value)) throw new HTTPException("Invalid value type, got $value_var_type, expected object", 400); if ($value !== array() and !array_assoc($value)) @@ -129,7 +141,7 @@ 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('Incorrect keys type'); + throw new HTTPException('Incorrect keys type'); // else set it at the corresponding value $obj = $this->read(true); @@ -137,10 +149,12 @@ public function setBulk($keys, $values) { // 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 ($key_var_type != 'string' and $key_var_type != 'double' and $key_var_type != 'integer') - throw new Exception('Incorrect key'); + 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]); @@ -157,7 +171,8 @@ private function newLastKey($arr) { $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(); + while (array_key_exists($last_key, $arr)) + $last_key = uniqid(); } return strval($last_key); @@ -165,7 +180,7 @@ private function newLastKey($arr) { public function add($value) { if ($this->autoKey == false) - throw new Exception('Automatic key generation is disabled'); + throw new HTTPException('Automatic key generation is disabled'); // restricts types to objects only $value_type = gettype($value); @@ -185,7 +200,7 @@ public function add($value) { public function addBulk($values) { if (!$this->autoKey) - throw new Exception('Automatic key generation is disabled'); + throw new HTTPException('Automatic key generation is disabled'); if ($values !== array() and $values == NULL) throw new HTTPException('null-like value not accepted', 400); @@ -199,16 +214,13 @@ public function addBulk($values) { // now the values inside this array must not be base values foreach ($values as $value) { $value_type = gettype($value); - if ( - is_primitive($value) or - (is_array($value) and count($value) and !array_assoc($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 Exception('Wanted sequential array'); + throw new HTTPException('Wanted sequential array'); // else set it at the corresponding value $obj = $this->read(true); @@ -230,8 +242,9 @@ public function addBulk($values) { } public function remove($key) { - if (gettype($key) != 'string') - throw new HTTPException('remove value must be a string', 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); $obj = $this->read(true); unset($obj['content'][$key]); @@ -247,8 +260,8 @@ public function removeBulk($keys) { 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); + 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]); } @@ -256,14 +269,13 @@ public function removeBulk($keys) { $obj = $this->read(true); // remove all keys - foreach ($keys as $key_decoded) { + foreach ($keys as $key_decoded) unset($obj['content'][$key_decoded]); - } $this->write($obj); } - private function _search($field, $criteria, $value, $ignoreCase) { + private function _search($field, $criteria, $value, $ignoreCase): bool { $fieldType = gettype($field); switch ($fieldType) { case 'boolean': @@ -321,13 +333,13 @@ private function _search($field, $criteria, $value, $ignoreCase) { $end = substr($field, -strlen($value)); return $value != '' ? ($cmpFunc($end, $value) === 0) : true; case 'in': - $notFound = true; - $a_i = 0; - while ($a_i < count($value) && $notFound) { - $notFound = $cmpFunc($field, $value[$a_i]) != 0; - $a_i++; + $found = false; + foreach ($value as $val) { + $found = $cmpFunc($field, $val) == 0; + if ($found) + break; } - return !$notFound; + return $found; default: return false; } @@ -356,27 +368,29 @@ private function _search($field, $criteria, $value, $ignoreCase) { default: break; } + + // unknown type + return false; } public function search($conditions, $random = false) { $obj = $this->read(); - $res = []; - foreach (array_keys($obj['content']) as $key) { - $el = $obj['content'][$key]; + foreach ($obj['content'] as $key => $el) { $el_root = $el; $add = true; foreach ($conditions as $condition) { - // cleaner than a million breaks inside the switch statement - if (!$add) break; + if (!$add) + break; // extract field $field = $condition['field']; $field_path = explode('.', $field); - for ($field_ind = 0; $el != NULL && $field_ind + 1 < count($field_path); $field_ind += 1) { + // 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; @@ -385,7 +399,8 @@ public function search($conditions, $random = false) { $field = $field_path[$field_ind + 1]; } - if ($el == NULL || + if ( + $el == NULL || !array_key_exists($field, $el) || !array_key_exists('criteria', $condition) || !array_key_exists('value', $condition) @@ -395,12 +410,19 @@ public function search($conditions, $random = false) { } $ignoreCase = array_key_exists('ignoreCase', $condition) && !!$condition['ignoreCase']; - $add = $this->_search($el[$field], $condition['criteria'], $condition['value'], $ignoreCase); + $add = $this->_search( + $el[$field], + $condition['criteria'], + $condition['value'], + $ignoreCase + ); $el = $el_root; } - if ($add) $res[$key] = $el_root; + // if all conditions are met, we can add the value to our output + if ($add) + $res[$key] = $el_root; } if ($random !== false) { @@ -411,7 +433,7 @@ public function search($conditions, $random = false) { throw new HTTPException('Seed not an integer value for random search result'); $seed = intval($rawSeed); } - $res = chooseRandom($res, $seed); + $res = choose_random($res, $seed); } return $res; @@ -447,7 +469,7 @@ private function _edit(&$obj, $editObj) { $id = $editObj['id']; // id string or integer - if (gettype($id) != 'string' and gettype($id) != 'integer') + if (!is_keyable($id)) return false; // object not found @@ -473,14 +495,20 @@ private function _edit(&$obj, $editObj) { $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 !isset($editObj['value'])) + if ( + in_array($operation, ['set', 'append', 'array-push', 'array-delete', 'array-splice']) 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')) + if ( + !isset($obj['content'][$id][$field]) and + ($operation != 'set' and $operation != 'remove' and $operation != 'array-push') + ) return false; switch ($operation) { @@ -507,19 +535,19 @@ private function _edit(&$obj, $editObj) { case 'increment': case 'decrement': // check type number - if (gettype($obj['content'][$id][$field]) != 'integer' and gettype($obj['content'][$id][$field]) != 'double') + if (!is_number_like($obj['content'][$id][$field])) return false; - $change = $operation == 'increment' ? +1 : -1; + $change = $operation == 'increment' ? 1 : -1; // check if value if (isset($editObj['value'])) { - if (gettype($editObj['value']) == 'integer' or gettype($editObj['value']) == 'double') { // error here + // error here + if (is_number_like($editObj['value'])) $change *= $editObj['value']; - } else { - // incorrect value provided, no operation done + // incorrect value provided, no operation done + else return false; - } } $obj['content'][$id][$field] += $change; @@ -562,7 +590,12 @@ private function _edit(&$obj, $editObj) { return false; // 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') + if ( + array_assoc($value) or + count($value) < 2 or + gettype($value[0]) != 'integer' or + gettype($value[1]) != 'integer' + ) return false; if (count($value) > 2) @@ -584,7 +617,8 @@ public function editField($editObj) { public function editFieldBulk($objArray) { // need sequential array - if (array_assoc($objArray)) return false; + if (array_assoc($objArray)) + return false; $arrayResult = array(); @@ -600,7 +634,8 @@ public function editFieldBulk($objArray) { } public function select($selectObj) { - if (!array_key_exists('fields', $selectObj)) throw new HTTPException('Missing required fields field'); + 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'); @@ -614,12 +649,12 @@ public function select($selectObj) { $obj = $this->read(); - $json = $obj['content']; $result = array(); - foreach ($json as $key => $value) { + foreach ($obj['content'] as $key => $value) { $result[$key] = array(); foreach ($fields as $field) { - if (array_key_exists($field, $value)) $result[$key][$field] = $value[$field]; + if (array_key_exists($field, $value)) + $result[$key][$field] = $value[$field]; } } @@ -645,16 +680,17 @@ public function values($valueObj) { $obj = $this->read(); - $json = $obj['content']; $result = []; - foreach ($json as $value) { + 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; + 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]); + else + array_push($result, $value[$field]); } // remove complex duplicates diff --git a/php/classes/read/random.php b/php/classes/read/random.php index be51653..4909ccf 100644 --- a/php/classes/read/random.php +++ b/php/classes/read/random.php @@ -35,10 +35,10 @@ function random($params, $class) { $json = $class->read()['content']; - return chooseRandom($json, $seed, $max, $offset); + return choose_random($json, $seed, $max, $offset); } -function chooseRandom($json, $seed = false, $max = -1, $offset = 0) { +function choose_random($json, $seed = false, $max = -1, $offset = 0) { $keys = array_keys($json); $keys_selected = array(); $keys_length = count($keys); diff --git a/php/utils.php b/php/utils.php index 75e2fd3..91ecb71 100644 --- a/php/utils.php +++ b/php/utils.php @@ -40,6 +40,15 @@ function is_primitive($value) { $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); } @@ -51,7 +60,7 @@ function check_key_json($key, $arr, $parse = false) { } function array_assoc(array $arr) { - if (array() === $arr) return false; + if (array() === $arr || !is_array($arr)) return false; return array_keys($arr) !== range(0, count($arr) - 1); } diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 9ec19ed..509b862 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -56,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", () => { @@ -179,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(); }); @@ -215,8 +215,8 @@ describe("GET operations", () => { 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) => { @@ -301,7 +301,7 @@ describe("GET operations", () => { describe("search(searchOptions)", () => { // [criteria, field, value, idsFound] - const test_array = [ + const testArray = [ ["!=", "age", 13, ["0", "2"]], ["==", "age", 13, ["1"]], ["==", "age", 25, []], @@ -344,32 +344,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.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(); @@ -466,7 +462,7 @@ describe("GET operations", () => { ], true, ) - .then((_) => done()) + .then(() => done()) .catch((err) => { console.error(err); done("Should not reject with error " + JSON.stringify(err)); @@ -547,18 +543,16 @@ describe("GET operations", () => { }); 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]; } }); }); - const selectResult = results[1]; Object.keys(selectResult).forEach((k) => { delete selectResult[k][firestorm.ID_FIELD]; }); @@ -744,7 +738,7 @@ describe("POST operations", () => { }); describe("You must give a correct value", () => { - const incorrect_bodies = [ + const incorrectBodies = [ undefined, null, false, @@ -756,7 +750,7 @@ describe("POST operations", () => { { 5: "is" }, ]; - incorrect_bodies.forEach((body, index) => { + incorrectBodies.forEach((body, index) => { it(`${JSON.stringify(body)} value rejects`, (done) => { base .writeRaw(body) @@ -823,7 +817,7 @@ describe("POST 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", @@ -833,17 +827,17 @@ describe("POST 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(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) => { + incorrectValues.forEach((unco) => { it(`${JSON.stringify(unco)} value rejects`, (done) => { base .add(unco) @@ -862,7 +856,7 @@ describe("POST operations", () => { }); describe("It should accept correct values", () => { - const correct_values = [ + const correctValues = [ {}, { name: "Elliot Alderson", @@ -872,7 +866,7 @@ describe("POST operations", () => { }, ]; - correct_values.forEach((co, index) => { + correctValues.forEach((co, index) => { it(`${index === 0 ? "Empty object" : "Complex object"} should fulfill`, (done) => { base .add(co) @@ -900,20 +894,15 @@ describe("POST 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) => { + incorrectValues.forEach((unco) => { it(`${JSON.stringify(unco)} value rejects`, (done) => { base .addBulk(unco) - .then((res) => { - done(new Error(`Should not fulfill with res ${res}`)); - }) + .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)}`)); }); }); @@ -921,9 +910,9 @@ describe("POST 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) => { + incorrectValues.forEach((unco) => { it(`[${JSON.stringify(unco)}] value rejects`, (done) => { base .addBulk([unco]) @@ -961,9 +950,9 @@ describe("POST operations", () => { }); 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.an("array"); expect(res).to.have.length(3); @@ -973,19 +962,18 @@ describe("POST operations", () => { return Promise.all([res, base.searchKeys(res)]); }) .then((results) => { - const search_results = results[1]; - expect(search_results).to.be.an("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) => { @@ -997,12 +985,11 @@ describe("POST 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], @@ -1010,7 +997,7 @@ describe("POST operations", () => { { "i'm": "batman" }, ]; - incorrect_values.forEach((unco) => { + incorrectValues.forEach((unco) => { it(`${JSON.stringify(unco)} value rejects`, (done) => { base .remove(unco) @@ -1060,9 +1047,9 @@ describe("POST 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) => { + incorrectValues.forEach((unco) => { it(`[${JSON.stringify(unco)}] value rejects`, (done) => { base .removeBulk([unco]) @@ -1125,15 +1112,13 @@ describe("POST operations", () => { }); describe("0 values fulfill", () => { - const correct_values = ["0", 0, 0.0]; + const correctValues = ["0", 0, 0.0]; - correct_values.forEach((unco) => { + correctValues.forEach((unco) => { it(`${JSON.stringify(unco)} value fulfills`, (done) => { base .set(unco, tmp) - .then((res) => { - done(); - }) + .then(() => done()) .catch((err) => { if ("response" in err) console.log(err.response.data); done(new Error(err)); @@ -1143,9 +1128,9 @@ describe("POST 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) => { + incorrectValues.forEach((unco) => { it(`${JSON.stringify(unco)} value rejects`, (done) => { base .set(unco, tmp) @@ -1163,9 +1148,9 @@ describe("POST 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) => { + incorrectValues.forEach((unco) => { it(`${JSON.stringify(unco)} value rejects`, (done) => { base .set("1", unco) @@ -1253,9 +1238,9 @@ describe("POST 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) => { + incorrectValues.forEach((unco) => { it(`[${JSON.stringify(unco)}] value rejects`, (done) => { base .setBulk([unco], [tmp]) @@ -1297,9 +1282,9 @@ describe("POST operations", () => { }); describe("Reject with incorrect values", () => { - const incorrect_values = [undefined, null, false, 16, 0.5, "", "gg", [], {}]; + const incorrectValues = [undefined, null, false, 16, 0.5, "", "gg", [], {}]; - incorrect_values.forEach((unco) => { + incorrectValues.forEach((unco) => { it(`'${JSON.stringify(unco)}' value rejects`, (done) => { base .editField(unco) From c0d41e41ff4444b1f4e28cddea183496ac0c4b18 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Mon, 29 Apr 2024 22:03:15 -0700 Subject: [PATCH 24/30] revert sec() and token changes --- php/classes/JSONDatabase.php | 2 +- php/files.php | 6 +++--- php/get.php | 2 +- php/post.php | 2 +- php/utils.php | 6 +++++- tests/tokens.php | 6 +++++- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index a912fa3..35b5f6b 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -428,7 +428,7 @@ public function search($conditions, $random = false) { if ($random !== false) { $seed = false; if (is_array($random) && array_key_exists('seed', $random)) { - $rawSeed = htmlspecialchars($random['seed']); + $rawSeed = sec($random['seed']); if (!is_int($rawSeed)) throw new HTTPException('Seed not an integer value for random search result'); $seed = intval($rawSeed); diff --git a/php/files.php b/php/files.php index edccbec..9ead278 100644 --- a/php/files.php +++ b/php/files.php @@ -13,7 +13,7 @@ require_once './utils.php'; require_once './log.php'; -$method = htmlspecialchars($_SERVER['REQUEST_METHOD']); +$method = sec($_SERVER['REQUEST_METHOD']); if ($method !== 'GET' && $method !== 'POST' && $method !== 'DELETE') { http_error(400, "Incorrect request type, expected GET, POST or DELETE, not $method"); } @@ -194,7 +194,7 @@ function p($var) { } catch (Throwable $th) { return false; } - return htmlspecialchars($_POST[$var]); + return sec($_POST[$var]); } function g($var) { @@ -204,5 +204,5 @@ function g($var) { return false; } - return htmlspecialchars($_GET[$var]); + return sec($_GET[$var]); } diff --git a/php/get.php b/php/get.php index 3b4010b..3da9407 100644 --- a/php/get.php +++ b/php/get.php @@ -11,7 +11,7 @@ // import useful functions require_once './log.php'; -$method = htmlspecialchars($_SERVER['REQUEST_METHOD']); +$method = sec($_SERVER['REQUEST_METHOD']); if ($method !== 'GET' && $method !== 'POST') { http_error(400, "Incorrect request type, expected GET or POST, not $method"); } diff --git a/php/post.php b/php/post.php index 1e7f097..7d79347 100644 --- a/php/post.php +++ b/php/post.php @@ -6,7 +6,7 @@ cors(); -$method = htmlspecialchars($_SERVER['REQUEST_METHOD']); +$method = sec($_SERVER['REQUEST_METHOD']); if ($method === 'GET') { http_error(400, "Incorrect request type, expected POST, not $method"); } diff --git a/php/utils.php b/php/utils.php index 91ecb71..05fbc3c 100644 --- a/php/utils.php +++ b/php/utils.php @@ -10,6 +10,10 @@ 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); @@ -55,7 +59,7 @@ function http_success($message) { function check_key_json($key, $arr, $parse = false) { if (array_key_exists($key, $arr)) - return $parse ? htmlspecialchars($arr[$key]) : $arr[$key]; + return $parse ? sec($arr[$key]) : $arr[$key]; return false; } diff --git a/tests/tokens.php b/tests/tokens.php index 0966c63..eea8d5b 100644 --- a/tests/tokens.php +++ b/tests/tokens.php @@ -1,3 +1,7 @@ 'NeverGonnaGiveYouUp' +); + +$db_tokens = array_values($db_tokens_map); From d2c06aa504860a588c3723539a4ff9c9dd31c059 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Tue, 30 Apr 2024 07:51:46 -0700 Subject: [PATCH 25/30] array-contains-none --- CHANGELOG.md | 5 +++-- README.md | 41 ++++++++++++++++++------------------ php/classes/JSONDatabase.php | 2 ++ src/index.js | 2 +- tests/js-test.spec.js | 9 +++++--- typings/index.d.ts | 1 + 6 files changed, 34 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d1d476..177d0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Exposed `Collection.collectionName` as a readonly property for TypeScript usage. - TypeScript overview to README.md. - 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. @@ -26,8 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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`s sometimes. -- `files.upload` not allowing the `form-data` package's typing of `FormData`. +- `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. diff --git a/README.md b/README.md index ca38884..4f0cfc4 100644 --- a/README.md +++ b/README.md @@ -91,26 +91,27 @@ There are more options available than the firestore `where` command, allowing yo 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-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 | +| 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 diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index 35b5f6b..efc02bd 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -347,6 +347,8 @@ private function _search($field, $criteria, $value, $ignoreCase): bool { 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': diff --git a/src/index.js b/src/index.js index 38651b3..829628c 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,7 @@ try { /** * @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) */ diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 509b862..8a8e58a 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -300,7 +300,7 @@ describe("GET operations", () => { }); describe("search(searchOptions)", () => { - // [criteria, field, value, idsFound] + // [criteria, field, value, idsFound, ignoreCase] const testArray = [ ["!=", "age", 13, ["0", "2"]], ["==", "age", 13, ["1"]], @@ -328,6 +328,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"], []], @@ -944,7 +947,7 @@ describe("POST operations", () => { done(); }) .catch((err) => { - console.log(err); + console.error(err); done(err); }); }); @@ -1120,7 +1123,7 @@ describe("POST operations", () => { .set(unco, 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)); }); }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 601cf3f..59bcc61 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -24,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 */ From 2cde398af37b4b68ff4197d9d7fd90baa248e168 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Tue, 30 Apr 2024 18:49:12 -0700 Subject: [PATCH 26/30] better browser check, last-minute readme changes --- README.md | 34 ++++++++++++++++++---------------- package.json | 2 +- src/index.js | 31 +++++++++++++------------------ 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 4f0cfc4..fbe0c88 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ -_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 @@ -29,7 +29,7 @@ Information about installing Firestorm server-side is given in the [PHP](#php-ba # JavaScript Client -The JavaScript [index.js](./src/index.js) file is simply an [Axios](https://www.npmjs.com/package/axios) wrapper of the PHP API. +The JavaScript [index.js](./src/index.js) file is simply an [Axios](https://www.npmjs.com/package/axios) wrapper of the PHP backend. ## JavaScript setup @@ -50,10 +50,10 @@ Now you can use Firestorm to its full potential. ## Create your first collection -Firestorm is based around the concept of a `Collection`, which is akin to an SQL table or Firestore document. A Firestorm collection takes one required argument and one optional argument in its constructor: +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. +- 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"); @@ -87,7 +87,7 @@ johnDoe.hello(); // "John Doe says hello!" ## Search options -There are more options available than the firestore `where` command, allowing you to get better and faster search results. +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. @@ -127,20 +127,20 @@ The search method can take one or more options to filter entries in a collection | 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 operations +## 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 | Allowed value types | 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 array element by index. Check the PHP [array_splice](https://www.php.net/manual/function.array-splice) documentation for more info. | +| `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. @@ -182,7 +182,7 @@ $database_list[$tmp->fileName] = $tmp; - 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 will be called in JavaScript in the Collection constructor (this can be different from the JSON filename if needed). +- 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. If you're working with multiple collections, it's probably easier to initialize them all in the array constructor directly: @@ -374,7 +374,7 @@ As it's entirely a JavaScript construct, `ID_FIELD` values will never be in your 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 configuration will disable `add` operations: +For instance, the following PHP configuration will disable add operations: ```php $database_list['users'] = new JSONDatabase('users', false); @@ -420,6 +420,8 @@ const johnDoe = await customers.get(123456789); 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. diff --git a/package.json b/package.json index 4686978..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" diff --git a/src/index.js b/src/index.js index 829628c..b58ef02 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,8 @@ -// browser check +const IS_NODE = typeof process === "object"; + try { - if (typeof process === "object") var axios = require("axios").default; + // ambient axios context in browser + if (IS_NODE) var axios = require("axios").default; } catch {} /** @@ -144,12 +146,9 @@ class Collection { command: command, ...data, }; - const request = - typeof process === "object" - ? axios.get(readAddress(), { - data: obj, - }) - : axios.post(readAddress(), obj); + 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); @@ -251,20 +250,16 @@ class Collection { if (!Array.isArray(options)) return Promise.reject(new TypeError("searchOptions shall be an array")); - options.forEach((searchOption) => { - if ( - searchOption.field === undefined || - searchOption.criteria === undefined || - searchOption.value === undefined - ) + 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 searchOption.field !== "string") + if (typeof option.field !== "string") return Promise.reject( - new TypeError(`${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)) + 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 @@ -637,5 +632,5 @@ const firestorm = { // browser check try { - if (typeof process === "object") module.exports = firestorm; + if (IS_NODE) module.exports = firestorm; } catch {} From dd17eda8407aa05eac9d75c48b7bdd4fe9c43bed Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Thu, 2 May 2024 17:11:33 -0700 Subject: [PATCH 27/30] readRaw(original = false) --- CHANGELOG.md | 3 ++- README.md | 42 ++++++++++++++++----------------- src/index.js | 55 +++++++++++++++++++++++++------------------ tests/js-test.spec.js | 19 +++++++++++++-- typings/index.d.ts | 4 ++-- 5 files changed, 74 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 177d0ed..0842f73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Exposed `Collection.collectionName` as a readonly property for TypeScript usage. -- TypeScript overview to README.md. +- 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 diff --git a/README.md b/README.md index fbe0c88..ffa3c52 100644 --- a/README.md +++ b/README.md @@ -74,16 +74,16 @@ johnDoe.hello(); // "John Doe says hello!" ## 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() | none | Read the entire collection. ID values are injected for easier iteration, so this may be different from sha1. | -| 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. | +| 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 @@ -131,17 +131,17 @@ The search method can take one or more options to filter entries in a collection 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 | 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. | +| 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. diff --git a/src/index.js b/src/index.js index b58ef02..ada4016 100644 --- a/src/index.js +++ b/src/index.js @@ -95,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; } @@ -106,19 +106,24 @@ class Collection { * Add user methods to returned data * @private * @ignore - * @param {AxiosPromise} req - Incoming request - * @returns {Promise} + * @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); } /** @@ -217,7 +222,12 @@ class Collection { get(key) { return this.__get_request("get", { id: key, - }).then((res) => this.__add_methods(res)); + }).then((res) => { + const firstKey = Object.keys(res)[0]; + res[firstKey][ID_FIELD_NAME] = firstKey; + res = res[firstKey]; + return this.__add_methods(res, false); + }); } /** @@ -294,18 +304,19 @@ class Collection { /** * Read the entire collection - * - ID values are injected for easier iteration, so this may be different from {@link sha1} + * @param {boolean} [original] - Disable ID field injection for easier iteration (default false) * @returns {Promise>} The entire collection */ - readRaw() { + 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); }); } @@ -332,9 +343,8 @@ class Collection { }).then((data) => { Object.keys(data).forEach((key) => { data[key][ID_FIELD_NAME] = key; - this.addMethods(data[key]); }); - return data; + return this.__add_methods(data); }); } @@ -395,10 +405,9 @@ class Collection { }).then((data) => { Object.keys(data).forEach((key) => { data[key][ID_FIELD_NAME] = key; - this.addMethods(data[key]); }); - return data; + return this.__add_methods(data); }); } diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index 8a8e58a..c996b2e 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -200,17 +200,32 @@ 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(done); }); + }); + describe("sha1()", () => { it("sha1 content hash is the same", (done) => { base .sha1() @@ -348,7 +363,7 @@ describe("GET operations", () => { ]; testArray.forEach(([criteria, field, value, idsFound, ignoreCase]) => { - ignoreCase = !!ignoreCase + ignoreCase = !!ignoreCase; it(`${criteria} criteria${idsFound.length == 0 ? " (empty result)" : ""}${ ignoreCase ? " (case insensitive)" : "" }`, (done) => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 59bcc61..bd96dce 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -228,10 +228,10 @@ declare class Collection { /** * Read the entire collection - * - ID values are injected for easier iteration, so this may be different from {@link sha1} + * @param original - Disable ID field injection for easier iteration (default false) * @returns The entire collection */ - public readRaw(): Promise>; + public readRaw(original?: boolean): Promise>; /** * Read the entire collection From a39b98f661f272b3602822921502b90f2ea80c74 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Sat, 4 May 2024 16:08:21 -0700 Subject: [PATCH 28/30] fix many editField problems also improve tests substantially --- CHANGELOG.md | 1 + php/classes/JSONDatabase.php | 86 +++++++------- php/utils.php | 4 +- tests/js-test.spec.js | 209 +++++++++++++++++++---------------- 4 files changed, 161 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0842f73..35481f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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 and poor error logging. ## [1.12.0] - 2024-02-22 diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index efc02bd..6021afe 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -460,58 +460,61 @@ public function searchKeys($searchedKeys) { } // MANDATORY REFERENCE to edit directly: PHP 5+ - private function _edit(&$obj, $editObj) { - if (!is_object($editObj)) - return false; + 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 (!check($editObj['id'])) - return false; + 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)) - return false; + 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])) - return false; + throw new HTTPException('ID doesn\'t exist in collection', 400); // field required - if (!check($editObj['field'])) - return false; + 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') - return false; + throw new HTTPException('field must be a string', 400); // operation required - if (!check($editObj['operation'])) - return false; + 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 - !isset($editObj['value']) + (!array_key_exists('value', $editObj) or !isset($editObj['value'])) ) - return false; - else + throw new HTTPException("A value is required for operation $operation", 400); + else if (array_key_exists('value', $editObj)) $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 + // 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') ) - return false; + throw new HTTPException("Field $field doesn't exist in ID $id", 400); switch ($operation) { case 'set': @@ -523,14 +526,14 @@ private function _edit(&$obj, $editObj) { case 'append': // check type string if (gettype($obj['content'][$id][$field]) != 'string' or gettype($value) != 'string') - return false; + 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') - return false; + throw new HTTPException('invert field must be a boolean', 400); $obj['content'][$id][$field] = !$obj['content'][$id][$field]; return true; @@ -538,7 +541,7 @@ private function _edit(&$obj, $editObj) { case 'decrement': // check type number if (!is_number_like($obj['content'][$id][$field])) - return false; + throw new HTTPException('increment and decrement fields must be numbers', 400); $change = $operation == 'increment' ? 1 : -1; @@ -549,7 +552,7 @@ private function _edit(&$obj, $editObj) { $change *= $editObj['value']; // incorrect value provided, no operation done else - return false; + throw new HTTPException('increment and decrement values must be numbers', 400); } $obj['content'][$id][$field] += $change; @@ -560,12 +563,11 @@ private function _edit(&$obj, $editObj) { $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; + 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); @@ -573,23 +575,22 @@ private function _edit(&$obj, $editObj) { 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; + 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') - return false; + 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])) - return false; + throw new HTTPException('array-splice field must be an array', 400); // value must be an array starting with two integers if ( @@ -598,7 +599,7 @@ private function _edit(&$obj, $editObj) { gettype($value[0]) != 'integer' or gettype($value[1]) != 'integer' ) - return false; + throw new HTTPException('Incorrect array-splice options', 400); if (count($value) > 2) array_splice($obj['content'][$id][$field], $value[0], $value[1], $value[2]); @@ -610,11 +611,14 @@ private function _edit(&$obj, $editObj) { break; } - return false; + throw new HTTPException("Edit failed on field $field with ID $id", 400); } public function editField($editObj) { - return $this->editFieldBulk(array($editObj))[0]; + $fileObj = $this->read(true); + $res = (bool) $this->_edit($fileObj, $editObj); + $this->write($fileObj); + return $res; } public function editFieldBulk($objArray) { @@ -624,10 +628,10 @@ public function editFieldBulk($objArray) { $arrayResult = array(); - $fileObj = $this->read($this); + $fileObj = $this->read(true); foreach ($objArray as &$editObj) { - array_push($arrayResult, $this->_edit($fileObj, $editObj)); + array_push($arrayResult, (bool) $this->_edit($fileObj, $editObj)); } $this->write($fileObj); diff --git a/php/utils.php b/php/utils.php index 05fbc3c..b9beb15 100644 --- a/php/utils.php +++ b/php/utils.php @@ -63,12 +63,12 @@ function check_key_json($key, $arr, $parse = false) { return false; } -function array_assoc(array $arr) { +function array_assoc($arr) { if (array() === $arr || !is_array($arr)) return false; return array_keys($arr) !== range(0, count($arr) - 1); } -function array_sequential(array $arr) { +function array_sequential($arr) { return !array_assoc($arr); } diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index c996b2e..cbe5361 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -442,16 +442,14 @@ describe("GET operations", () => { ]); done(); }) - .catch((err) => { - done(err); - }); + .catch((err) => done(err)); }); }); // undefined works because random becomes default parameter false, so false works too const incorrect = [null, "gg", ""]; - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} seed rejects`, (done) => { + incorrect.forEach((incor) => { + it(`${JSON.stringify(incor)} seed rejects`, (done) => { base .search( [ @@ -461,7 +459,7 @@ describe("GET operations", () => { value: "", }, ], - unco, + incor, ) .then((res) => done(`got ${JSON.stringify(res)} value`)) .catch(() => done()); @@ -526,10 +524,10 @@ describe("GET operations", () => { describe("requires field to be a string array", () => { // all incorrect values must catch const incorrect = [undefined, null, false, 5, 12.5, "gg", { toto: "tata" }]; - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} value rejects`, (done) => { + 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()); }); @@ -550,11 +548,11 @@ 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 rejects`, (done) => { + incorrect.forEach((incor) => { + it(`[${JSON.stringify(incor)}] value rejects`, (done) => { base - .select({ fields: [unco] }) - .then(() => done(`[${JSON.stringify(unco)}] value passed`)) + .select({ fields: [incor] }) + .then(() => done(`[${JSON.stringify(incor)}] value passed`)) .catch(() => done()); }); }); @@ -606,10 +604,10 @@ describe("GET operations", () => { describe("needs string field value", () => { const incorrect = [null, false, 5.5, -5, { key: "val" }, ["asdf"]]; - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} value rejects`, (done) => { + incorrect.forEach((incor) => { + it(`${JSON.stringify(incor)} value rejects`, (done) => { base - .values({ field: unco }) + .values({ field: incor }) .then(() => done("Did not expect it to succeed")) .catch(() => done()); }); @@ -692,10 +690,10 @@ describe("GET operations", () => { describe("requires max parameter to be an integer >= -1", () => { // all incorrect values must catch const 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) => { + incorrect.forEach((incor) => { + it(`${JSON.stringify(incor)} value`, (done) => { base - .random(unco) + .random(incor) .then((res) => done(`got ${JSON.stringify(res)} value`)) .catch(() => done()); }); @@ -705,10 +703,10 @@ describe("GET operations", () => { describe("requires seed parameter to be an integer", () => { // all incorrect values must catch const incorrect = [null, false, "gg", 5.5]; // undefined works because then seed is automatic - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} value`, (done) => { + 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()); }); @@ -725,10 +723,10 @@ describe("GET operations", () => { describe("requires offset parameter to be an integer >= 0", () => { // all incorrect values must catch const incorrect = [null, false, "gg", 5.5, -1]; // undefined works because then offset is 0 - incorrect.forEach((unco) => { - it(`${JSON.stringify(unco)} value`, (done) => { + 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()); }); @@ -855,10 +853,10 @@ describe("POST operations", () => { 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 - incorrectValues.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}`)); }) @@ -914,10 +912,10 @@ describe("POST operations", () => { describe("must reject with incorrect base values", () => { const incorrectValues = [undefined, null, false, 16, "Muse", [1, 2, 3]]; - incorrectValues.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}`))) .catch((err) => { if ("response" in err && err.response.status == 400) return done(); @@ -930,10 +928,10 @@ describe("POST operations", () => { describe("must reject with incorrect array", () => { const incorrectValues = [undefined, null, false, 16, "Muse", [1, 2, 3]]; - incorrectValues.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}`)); }) @@ -1015,10 +1013,10 @@ describe("POST operations", () => { { "i'm": "batman" }, ]; - incorrectValues.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)}`)); }) @@ -1067,10 +1065,10 @@ describe("POST operations", () => { describe("must accept only string array", () => { const incorrectValues = [undefined, null, false, [], [1, 2, 3], {}, { "i'm": "batman" }]; - incorrectValues.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)}`)); }) @@ -1132,10 +1130,10 @@ describe("POST operations", () => { describe("0 values fulfill", () => { const correctValues = ["0", 0, 0.0]; - correctValues.forEach((unco) => { - it(`${JSON.stringify(unco)} value fulfills`, (done) => { + correctValues.forEach((incor) => { + it(`${JSON.stringify(incor)} value fulfills`, (done) => { base - .set(unco, tmp) + .set(incor, tmp) .then(() => done()) .catch((err) => { if ("response" in err) console.error(err.response.data); @@ -1148,10 +1146,10 @@ describe("POST operations", () => { describe("Key must be a string or an integer", () => { const incorrectValues = [undefined, null, false, [], [1, 2, 3], {}, { "i'm": "batman" }]; - incorrectValues.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)}`)); }) @@ -1168,10 +1166,10 @@ describe("POST operations", () => { describe("Value must be an object", () => { const incorrectValues = [undefined, null, false, 16, 22.2, [1, 2, 3]]; - incorrectValues.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)}`)); }) @@ -1258,10 +1256,10 @@ describe("POST operations", () => { describe("keys should be an array of string", () => { const incorrectValues = [undefined, null, false, [], [1, 2, 3], {}, { "i'm": "batman" }]; - incorrectValues.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)}`)); }) @@ -1299,21 +1297,69 @@ describe("POST operations", () => { await resetDatabaseContent(); }); - describe("Reject with incorrect values", () => { - const incorrectValues = [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)}`)); + }); + }); + + 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"]], + ]; - incorrectValues.forEach((unco) => { - it(`'${JSON.stringify(unco)}' value rejects`, (done) => { + 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) => { - done(new Error("Should not fulfill with " + JSON.stringify(res))); + expect(res.success).to.equal(true, "Should not fail"); + return base.get(2); }) + .then((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)}`)); }); }); @@ -1328,7 +1374,7 @@ describe("POST 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]; }); @@ -1336,14 +1382,9 @@ describe("POST 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)}`)); }); }); @@ -1359,37 +1400,13 @@ describe("POST 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)}`)); - }); - }); }); }); From 6dd4aa25d2cb8219c6f78889b6fe36c5bc1a8cb8 Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Mon, 6 May 2024 23:14:05 -0700 Subject: [PATCH 29/30] editField operations return confirmations --- CHANGELOG.md | 4 +++- php/classes/JSONDatabase.php | 12 +++--------- php/post.php | 4 ++-- tests/js-test.spec.js | 5 ++++- typings/index.d.ts | 6 ++---- 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35481f7..eee39b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 @@ -36,7 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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 and poor error logging. +- `editField` and `editFieldBulk` validation issues. ## [1.12.0] - 2024-02-22 diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index 6021afe..96bf7df 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -616,9 +616,8 @@ private function _edit(&$obj, $editObj): bool { public function editField($editObj) { $fileObj = $this->read(true); - $res = (bool) $this->_edit($fileObj, $editObj); + $this->_edit($fileObj, $editObj); $this->write($fileObj); - return $res; } public function editFieldBulk($objArray) { @@ -626,17 +625,12 @@ public function editFieldBulk($objArray) { if (array_assoc($objArray)) return false; - $arrayResult = array(); - $fileObj = $this->read(true); - foreach ($objArray as &$editObj) { - array_push($arrayResult, (bool) $this->_edit($fileObj, $editObj)); + // edit by reference, faster than passing values back and forth + $this->_edit($fileObj, $editObj); } - $this->write($fileObj); - - return $arrayResult; } public function select($selectObj) { diff --git a/php/post.php b/php/post.php index 7d79347..e7fd557 100644 --- a/php/post.php +++ b/php/post.php @@ -119,14 +119,14 @@ 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) http_error(400, 'Incorrect data provided'); - http_message($res, 'success', 200); + http_success("Successful $command command"); break; default: break; diff --git a/tests/js-test.spec.js b/tests/js-test.spec.js index cbe5361..d6adc87 100644 --- a/tests/js-test.spec.js +++ b/tests/js-test.spec.js @@ -1338,7 +1338,10 @@ describe("POST operations", () => { base .editField(obj) .then((res) => { - expect(res.success).to.equal(true, "Should not fail"); + expect(res).to.deep.equal( + { message: "Successful editField command" }, + "Should not fail", + ); return base.get(2); }) .then((res) => { diff --git a/typings/index.d.ts b/typings/index.d.ts index bd96dce..16d7690 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -337,16 +337,14 @@ declare class Collection { * @param option - The edit object * @returns Edit confirmation */ - public editField(option: EditFieldOption>): Promise<{ success: boolean }>; + public editField(option: EditFieldOption>): Promise; /** * Edit multiple elements' fields in the collection * @param options - The edit objects * @returns Edit confirmation */ - public editFieldBulk( - options: EditFieldOption>[], - ): Promise<{ success: boolean[] }>; + public editFieldBulk(options: EditFieldOption>[]): Promise; } /** Value for the ID field when searching content */ From ab42fe89aa2613cc603b65576764bff57f5e70ee Mon Sep 17 00:00:00 2001 From: Evorp <3vorpgaming@gmail.com> Date: Mon, 6 May 2024 23:36:03 -0700 Subject: [PATCH 30/30] fix unknown operation message, jsdoc --- CHANGELOG.md | 2 +- php/classes/JSONDatabase.php | 2 +- php/get.php | 1 + php/post.php | 2 +- src/index.js | 6 +++--- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eee39b7..3c2d9da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 return confirmations like all other write methods. - `editField` and `editFieldBulk` now reject with a descriptive error message on failure rather than silently failing. ### Fixed diff --git a/php/classes/JSONDatabase.php b/php/classes/JSONDatabase.php index 96bf7df..6f26a8f 100644 --- a/php/classes/JSONDatabase.php +++ b/php/classes/JSONDatabase.php @@ -611,7 +611,7 @@ private function _edit(&$obj, $editObj): bool { break; } - throw new HTTPException("Edit failed on field $field with ID $id", 400); + throw new HTTPException("Unknown operation $operation", 400); } public function editField($editObj) { diff --git a/php/get.php b/php/get.php index 3da9407..abfa554 100644 --- a/php/get.php +++ b/php/get.php @@ -31,6 +31,7 @@ // import db config require_once './config.php'; +// HTTPExceptions get properly handled in the catch try { // checking good collection diff --git a/php/post.php b/php/post.php index e7fd557..43e6335 100644 --- a/php/post.php +++ b/php/post.php @@ -43,7 +43,7 @@ // import db config require_once './config.php'; -// trying things +// HTTPExceptions get properly handled in the catch try { // checking good collection diff --git a/src/index.js b/src/index.js index ada4016..5dd9894 100644 --- a/src/index.js +++ b/src/index.js @@ -33,7 +33,7 @@ try { */ /** - * @typedef {WriteConfirmation} + * @typedef {Object} WriteConfirmation * @property {string} message - Write status */ @@ -508,7 +508,7 @@ class Collection { /** * Edit an element's field in the collection * @param {EditFieldOption} option - The edit object - * @returns {Promise<{ success: boolean }>} Edit confirmation + * @returns {Promise} Edit confirmation */ editField(option) { const data = this.__write_data("editField", option, null); @@ -518,7 +518,7 @@ class Collection { /** * Edit multiple elements' fields in the collection * @param {EditFieldOption[]} options - The edit objects - * @returns {Promise<{ success: boolean[] }>} Edit confirmation + * @returns {Promise} Edit confirmation */ editFieldBulk(options) { const data = this.__write_data("editFieldBulk", options, undefined);