const Node = require('../structures/Node'); const Dict = require('../structures/Dict'); const build = require('../util/query'); const endpoints = require('./endpoints'); class NodeManager { /** * Allowed filter arguments for nodes. */ static get FILTERS() { return Object.freeze(['uuid', 'name', 'fqdn', 'daemon_token_id']); } /** * Allowed include arguments for nodes. */ static get INCLUDES() { return Object.freeze(['allocations', 'location', 'servers']); } /** * Allowed sort arguments for nodes. */ static get SORTS() { return Object.freeze(['id', 'uuid', 'memory', 'disk']); } constructor(client) { this.client = client; /** @type {Dict} */ this.cache = new Dict(); } _patch(data) { if (data.data) { const res = new Dict(); for (const o of data.data) { const n = new Node(this.client, o); res.set(n.id, n); } if (this.client.options.nodes.cache) res.forEach((v, k) => this.cache.set(k, v)); return res; } const n = new Node(this.client, data); if (this.client.options.nodes.cache) this.cache.set(n.id, n); return n; } /** * Resolves a node from an object. This can be: * * a string * * a number * * an object * * Returns `undefined` if not found. * @param {string|number|object|Node} obj The object to resolve from. * @returns {?Node} The resolved node. */ resolve(obj) { if (obj instanceof Node) return obj; if (typeof obj === 'number') return this.cache.get(obj); if (typeof obj === 'string') return this.cache.find(n => n.name === obj); if (obj.relationships?.node) return this._patch(obj.relationships.node); return undefined; } /** * Returns a formatted URL to the node in the admin panel. * @param {number|Node} node The node or ID of the node. * @returns {string} The formatted URL. */ adminURLFor(node) { if (node instanceof Node) return node.adminURL; return `${this.client.domain}/admin/nodes/view/${node}`; } /** * Fetches a node from the Pterodactyl API with an optional cache check. * @param {number} [id] The ID of the node. * @param {object} [options] Additional fetch options. * @param {boolean} [options.force] Whether to skip checking the cache and fetch directly. * @param {string[]} [options.include] Additional data to include about the node. * @returns {Promise>} The fetched node(s). */ async fetch(id, options = {}) { if (id && !options.force) { const n = this.cache.get(id); if (n) return Promise.resolve(s); } const query = build(options, { include: NodeManager.INCLUDES }); const data = await this.client.requests.get( (id ? endpoints.nodes.get(id) : endpoints.nodes.main) + query ); return this._patch(data); } /** * Queries the API for a node (or nodes) that match the specified query filter/sort. * This does NOT check the cache first, it is a direct fetch from the API. * Available filters: * * uuid * * name * * fqdn * * daemonTokenId * * Available sort options: * * id * * -id * * uuid * * -uuid * * memory * * -memory * * disk * * -disk * * @param {string} entity The entity to query. * @param {string} filter The filter to use for the query. * @param {string} sort The order to sort the results in. * @returns {Promise>} A dict of the quiried nodes. */ async query(entity, filter, sort) { if (!sort && !filter) throw new Error('Sort or filter is required.'); if (filter === 'daemonTokenId') filter = 'daemon_token_id'; const { FILTERS, SORTS } = NodeManager; const query = build( { filter:[filter, entity], sort }, { filters: FILTERS, sorts: SORTS } ); const data = await this.client.requests.get(endpoints.nodes.main + query); return this._patch(data); } /** * Creates a new Pterodactyl server node. * @param {object} options Node creation options. * @param {string} options.name The name of the node. * @param {number} options.location The ID of the location for the node. * @param {string} options.fqdn The FQDN for the node. * @param {string} options.scheme The HTTP/HTTPS scheme for the node. * @param {number} options.memory The amount of memory for the node. * @param {number} options.disk The amount of disk for the node. * @param {object} options.sftp SFTP options. * @param {number} options.sftp.port The port for the SFTP. * @param {number} options.sftp.listener The listener port for the SFTP. * @param {number} [options.upload_size] The maximum upload size for the node. * @param {number} [options.memory_overallocate] The amount of memory over allocation. * @param {number} [options.disk_overallocate] The amount of disk over allocation. * @returns {Promise} The new node. */ async create(options = {}) { if ( !options.name || !options.location || !options.fqdn || !options.scheme || !options.memory || !options.disk || !options.sftp?.port || !options.sftp?.listener ) throw new Error('Missing required Node creation option.'); const payload = {}; payload.name = options.name; payload.location = options.location; payload.fqdn = options.fqdn; payload.scheme = options.scheme; payload.memory = options.memory; payload.disk = options.disk; payload.sftp = options.sftp; payload.upload_size = options.upload_size ?? 100; payload.memory_overallocate = options.memory_overallocate ?? 0; payload.disk_overallocate = options.disk_overallocate ?? 0; const data = await this.client.requests.post( endpoints.nodes.main, payload ); return this._patch(data); } /** * Updates a specified node. * @param {number|Node} node The node to update. * @param {object} options Node update options. * @param {string} [options.name] The name of the node. * @param {number} [options.location] The ID of the location for the node. * @param {string} [options.fqdn] The FQDN for the node. * @param {string} [options.scheme] The HTTP/HTTPS scheme for the node. * @param {number} [options.memory] The amount of memory for the node. * @param {number} [options.disk] The amount of disk for the node. * @param {object} [options.sftp] SFTP options. * @param {number} [options.sftp.port] The port for the SFTP. * @param {number} [options.sftp.listener] The listener port for the SFTP. * @param {number} [options.upload_size] The maximum upload size for the node. * @param {number} [options.memory_overallocate] The amount of memory over allocation. * @param {number} [options.disk_overallocate] The amount of disk over allocation. * @returns {Promise} The updated node instance. */ async update(node, options = {}) { if (typeof node === 'number') node = await this.fetch(node); if (!Object.keys(options).length) throw new Error('Too few options to update.'); const { id } = node; const payload = {}; Object.entries(node.toJSON()).forEach(e => payload[e[0]] = options[e[0]] ?? e[1]); payload.memory_overallocate = payload.overallocated_memory; payload.disk_overallocate = payload.overallocated_disk; const data = await this.client.requests.patch( endpoints.nodes.get(id), payload ); return this._patch(data); } /** * Deletes a node from Pterodactyl. * @param {number|Node} node The node to delete. * @returns {Promise} */ async delete(node) { if (node instanceof Node) node = node.id; await this.client.requests.delete(endpoints.nodes.get(node)); this.cache.delete(node); return true; } } module.exports = NodeManager;