index.js

const axios = require("axios").default;

const Collection = require("./collection.js");
const FirestormFiles = require("./files.js");

const { extractRequest } = require("./utils.js");

const ID_FIELD_NAME = "id";

/**
 * @typedef FirestormCreationOption
 * @property {string} [name] - Instance name (can be helpful for debugging)
 * @property {string} [address] - Firestorm server address
 * @property {token} [token] - Firestorm write token
 */

/**
 * Represents a Firestorm-powered server and its collections, tokens, and setup
 */
class Firestorm {
	/** @ignore */
	_name;
	/** @ignore */
	_address;
	/** @ignore */
	_token;

	/**
	 * Firestorm file manager
	 * @type {FirestormFiles}
	 */
	files;

	/**
	 * Create a new Firestorm instance
	 * - All parameters are optional and can be edited using the name, address, and token fields
	 * @param {FirestormCreationOption} [params] - Firestorm instance name, server address, and write token
	 */
	constructor({ name, address, token } = {}) {
		this.name = name;
		this.address = address;
		this.token = token;
		this.files = new FirestormFiles(this);
	}

	/**
	 * Create a new Firestorm collection instance
	 * @template T
	 * @param {string} name - The name of the collection
	 * @param {Function} [addMethods] - Additional methods and data to add to the objects
	 * @returns {Collection<T>} The collection instance
	 */
	collection(name, addMethods = (el) => el) {
		return new Collection(this, name, addMethods);
	}

	// jsdoc has a really hard time dealing with getters/setters so this makes it look decent

	/** @type {string} */
	get name() {
		return this._name || this.address;
	}

	/** @ignore */
	set name(newValue) {
		this._name = String(newValue);
	}

	name(newValue) {
		this.name = newValue;
		return this.name;
	}

	/** @type {string} */
	get token() {
		return this._token;
	}

	/** @ignore */
	set token(newValue) {
		this._token = newValue;
	}

	token(newValue) {
		this.token = newValue;
		return this.token;
	}

	/** @type {string} */
	get address() {
		return this._address;
	}

	/** @ignore */
	set address(newValue) {
		if (newValue && !newValue.endsWith("/")) newValue += "/";
		this._address = newValue;
	}

	address(newValue) {
		this.address = newValue;
		return this.address;
	}

	/**
	 * Get the current version of Firestorm
	 * @type {string}
	 */
	get clientVersion() {
		return require("../package.json").version;
	}

	/**
	 * Get the version of Firestorm used on the provided server
	 * @type {Promise<string>}
	 */
	get serverVersion() {
		if (!this.address)
			throw new Error(`Address for Firestorm instance "${this.instance.name}" was not configured`);

		return extractRequest(
			axios.get(`${this.address}version.php`, {
				data: {
					token: this.token,
				},
			}),
		);
	}

	/**
	 * Check whether the server-side Firestorm version is compatible with the client
	 * @returns {Promise<boolean>} - Whether the versions match
	 */
	async isCompatibleAddress() {
		const serverVersion = await this.serverVersion;
		const [serverMajor, serverMinor] = serverVersion.split(".");
		const [clientMajor, clientMinor] = this.clientVersion.split(".");

		// minor version keeps server compatibility (only added features), patch version is irrelevant
		return serverMajor === clientMajor && serverMinor >= clientMinor;
	}
}

/**
 * @namespace firestorm
 */
const firestorm = {
	/**
	 * Change or get the current Firestorm address
	 * @param {string} [newValue] - The new Firestorm address
	 * @returns {string} The stored Firestorm address
	 */
	address(newValue = undefined) {
		const current_address = firestorm.__default_instance.address;
		if (newValue === undefined && current_address === undefined)
			throw new Error("Firestorm address was not configured");

		if (newValue !== undefined && !newValue.endsWith("/")) newValue += "/";
		if (newValue !== undefined) firestorm.__default_instance.address = newValue;

		return firestorm.__default_instance.collection("_").__read_address;
	},

	/**
	 * Change or get the current Firestorm token
	 * @param {string} [newValue] - The new Firestorm write token
	 * @returns {string} The stored Firestorm write token
	 */
	token(newValue = undefined) {
		const current_token = firestorm.__default_instance.token;
		if (newValue === undefined && current_token === undefined)
			throw new Error("Firestorm token was not configured");
		if (newValue !== undefined) firestorm.__default_instance.token = newValue;
		return firestorm.__default_instance.token;
	},

	/**
	 * Create a new Firestorm collection instance
	 * @template T
	 * @param {string} name - The name of the collection
	 * @param {Function} [addMethods] - Additional methods and data to add to the objects
	 * @returns {Collection<T>} The collection instance
	 */
	collection(name, addMethods = (el) => el) {
		return firestorm.__default_instance.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<T>} The table instance
	 */
	table(name) {
		return this.collection(name);
	},

	/**
	 * Create as new instance of Firestorm
	 *
	 * @param {FirestormCreationOption} [params] - Firestorm instance name, server address, and write token
	 * @returns {Firestorm} New Firestorm instance
	 */
	create: function (params = {}) {
		return new Firestorm(params);
	},

	/** @type {Firestorm} */
	__default_instance: new Firestorm({ name: "__default" }),

	/** Value for the ID field when searching content */
	ID_FIELD: ID_FIELD_NAME,

	/**
	 * Firestorm file manager
	 * @type {FirestormFiles}
	 */
	get files() {
		return new FirestormFiles(firestorm.__default_instance);
	},
};

module.exports = firestorm;