From 0bd3d4f058287b3e26583ddcaed3fba49f44c46b Mon Sep 17 00:00:00 2001 From: Kyra Date: Sun, 25 Feb 2018 14:19:22 +0100 Subject: [PATCH] SettingGateway v2.1 (#179) * SGv2.1 refactor * Fixed deploy * Now for real * Make unused gateways lightning fast, updated changelog, fixed cache * SchemaPiece#modify -> edit, GatewayDriver#types type from string[] to Set, massive documentation overhaul * MDLint * Fixed deploy * Docs fixes * Removed ConfigUpdateEntryMany, tweaked docs, added toJSON to Gateway and GatewayDriver * only decent way to document Symbol.iterator * Requested changes * IterableIterator -> Iterator * Added iterator for Schedule * Updated changelog * Removed `SchemaFolder#getList` and replaced it to `Configuration#list`. * First functional version * Removed the abstract method `resolveString()` from SchemaFolder and SchemaPiece. * Fixed path.resolveString * whoops * Fixed disabledCommands' validator * Updated typings * Fixed Configuration#resolveString not parsing the keys correctly * removeKey/Folder -> remove, hasKey -> has, improvements, code reduction * More memory improvements, bugfixes * perf(iterators), removed Configuration#resetConfiguration * SchemaFolder#add * Forgot to call the function * Configuration#reset * Bugfixes * Update Client.js * Updated resolvers to not include Promise<> in returns when async * Removed Promise<> in all JSDocs for methods with async * Fixed SchemaFolder#add * Fixed old cache methods * Fixed Configuration#reset() not creating the right object * Added `Configuration#reset(string[]);` * Fixed an edge case in super-nested keys * Fixed deep merger * Fix Configuration._patch * Fixed critical bug in SGv2 shard support * Generate better SchemaPiece defaults * Fixed a bug * Better defaults * Fixed example running off the site * Fixed example for GatewayDriver#add * Give the example some sense * (typedef) emoji -> Emoji * Fixed Emoji in RM and RD, RichMenuEmojisObject extend RichDisplayEmojisObject * Way much safer and faster Configuration#_merge * Configuration refactor * Fix Configuration merge * Fix a little issue * Return a better error if getPath returns null in silent mode * Fix Gateway#getPath * Actually fix it * Whoops * Fixed save not accessing to the right tuple * Refactored the updateMany pattern * Revert accidental .eslintrc.json change * Fixed getPath, again * Fix getPath, finally * Fixed another typo in Configuration#_setValueByPath * Fix Configuration#_setValueByPath not updating correctly * Fixed Util.deepClone and SchemaFolder#_shardSyncSchema * Added overload for Configuration#reset, added COMMAND_CONF_NOCHANGE to language * Changelog * Ao's request * Add KlasaClient#options docs, updated events, fixed typo in KlasaUser docs * Typo * Removed all `@memberof` from jsdocs, removed duplicated/useless typedefs * Don't show folders if they don't have any configurable keys * Fixed Configuration#resolveString not getting the right values * Fixed ESLint * Fixed Configuration#get calling itself recursively instead of Configuration#_get * Fixed resetAll pattern not resetting nested keys * Typings fixes --- CHANGELOG.md | 32 + guides/.docconfig.json | 19 + .../SettingGatewayConfigurationUpdate.md | 38 ++ .../SettingGatewayKeyTypes.md | 66 ++ .../UnderstandingSchemaFolders.md | 142 +++++ .../UnderstandingSchemaPieces.md | 67 ++ .../UnderstandingSettingGateway.md | 261 +------- package.json | 2 +- src/commands/Admin/conf.js | 27 +- src/commands/General/User Configs/userconf.js | 27 +- src/events/configUpdateEntry.js | 8 +- src/languages/en-US.js | 1 + src/lib/Client.js | 53 +- src/lib/extensions/KlasaGuild.js | 2 +- src/lib/extensions/KlasaMessage.js | 6 +- src/lib/extensions/KlasaUser.js | 4 +- src/lib/parsers/ArgResolver.js | 60 +- src/lib/parsers/SettingResolver.js | 10 +- src/lib/permissions/PermissionLevels.js | 1 - src/lib/schedule/Schedule.js | 23 +- src/lib/schedule/ScheduledTask.js | 3 - src/lib/settings/Configuration.js | 596 +++++++++--------- src/lib/settings/Gateway.js | 156 ++--- src/lib/settings/GatewayDriver.js | 164 +++-- src/lib/settings/GatewayStorage.js | 16 +- src/lib/settings/SchemaFolder.js | 391 +++++------- src/lib/settings/SchemaPiece.js | 109 +--- src/lib/structures/Command.js | 3 +- src/lib/structures/Event.js | 1 - src/lib/structures/Extendable.js | 1 - src/lib/structures/Finalizer.js | 1 - src/lib/structures/Inhibitor.js | 3 +- src/lib/structures/Language.js | 1 - src/lib/structures/Monitor.js | 1 - src/lib/structures/Provider.js | 1 - src/lib/structures/Task.js | 5 +- src/lib/structures/base/Piece.js | 1 - src/lib/usage/TextPrompt.js | 13 +- src/lib/util/Colors.js | 3 - src/lib/util/KlasaConsole.js | 7 - src/lib/util/ReactionHandler.js | 10 +- src/lib/util/RichDisplay.js | 26 +- src/lib/util/RichMenu.js | 42 +- src/lib/util/Timestamp.js | 1 - src/lib/util/constants.js | 39 -- src/lib/util/util.js | 41 +- src/providers/collection.js | 154 ----- typings/index.d.ts | 174 ++--- 48 files changed, 1377 insertions(+), 1435 deletions(-) create mode 100644 guides/Advanced SettingGateway/SettingGatewayConfigurationUpdate.md create mode 100644 guides/Advanced SettingGateway/SettingGatewayKeyTypes.md create mode 100644 guides/Advanced SettingGateway/UnderstandingSchemaFolders.md create mode 100644 guides/Advanced SettingGateway/UnderstandingSchemaPieces.md delete mode 100644 src/providers/collection.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f033722f5..eec536d55e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,14 @@ NOTE: For the contributors, you add new entries to this document following this ### Added +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Added the key `COMMAND_CONF_NOCHANGE` to the **en-US** language file. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Added support for `Configuration#reset(string[]);` to reset multiple keys. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Added `util.arraysEqual`. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Added property `Symbol.iterator` to Schedule. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Added `Gateway#toJSON()` and `GatewayDriver#toJSON()`. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Added `GatewayDriver#register` to be able to register new gateways without events (directly in your `app.js`). (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Added `util.getIdentifier` as a replacement for the function validator. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Added `SchemaFolder#keys()`, `SchemaFolder#values()`, `SchemaFolder#entries()` and `SchemaFolder#[@@iterator]()`. Identical to Map's respective methods. (kyranet) - [[#201](https://github.com/dirigeants/klasa/pull/201)] Improve `util.toTitleCase`. (KingDGrizzle) - [[#186](https://github.com/dirigeants/klasa/pull/186)] Added a **load** command. (kyranet) - [[#176](https://github.com/dirigeants/klasa/pull/176)] Added `categorychannel` type to `ArgResolver`. (kyranet) @@ -83,6 +91,10 @@ NOTE: For the contributors, you add new entries to this document following this ### Changed +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Refactored Configuration's internals for maximum consistency and reduced code duplication. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Changed the type for `GatewayDriver#types` from `string[]` to `Set`. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Renamed `SchemaPiece#modify()` to `SchemaPiece#edit()`. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Renamed `Gateway#getKeys()` and `Gateway#getValues()` to `Gateway#keys(true)` and `Gateway#values(true)` respectively, which return iterators. (kyranet) - [[#184](https://github.com/dirigeants/klasa/pull/184)] `Piece#file` is now consistent between all pieces and it's type of `string[]`. (bdistin) - [[#184](https://github.com/dirigeants/klasa/pull/184)] **[MEM-PERF]** Tweaked `Command` to have category and subCategory properties as getters. (bdistin) - [[#184](https://github.com/dirigeants/klasa/pull/184)] Tweaked all stores to be able to load files in deep folders and abstracted it in `Store` for code reduction. (bdistin) @@ -139,6 +151,17 @@ NOTE: For the contributors, you add new entries to this document following this ### Removed +- [[#179](https://github.com/dirigeants/klasa/pull/179)] **[BREAKING]** Removed `SchemaFolder#addKey` and `SchemaFolder#addFolder` in favor to a more consistent `Schema#add`. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] **[BREAKING]** Removed `Configuration#resetConfiguration()`. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] **[PERF-MEM]** Removed `Configuration#type`. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] **[BREAKING]** Removed `SchemaFolder#removeKey` and `SchemaFolder#removeFolder` in favor to a more consistent `Schema#remove`. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Removed the abstract method `resolveString()` from SchemaFolder and SchemaPiece. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Removed `SchemaFolder#getList` and replaced it to `Configuration#list`. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Removed the `ConfigUpdateEntryMany` typedef in favor of a more constant type. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Removed the resolver functions from constants. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Removed `SchemaFolder#keys` (`Map`) to reduce RAM usage and key caching duplication. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Removed SettingGateway function validators. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Removed Collection cache provider (will be moved to klasa-pieces). (kyranet) - [[#159](https://github.com/dirigeants/klasa/pull/159)] Removed deprecated property `GatewayOptions.cache` to be locked to `'collection'`. (kyranet) - [[#158](https://github.com/dirigeants/klasa/pull/158)] `Configuration#updateMany` is now under `Configuration#update`, in favor of a much less confusing naming. (kyranet) - [[`5b0c468362`](https://github.com/dirigeants/klasa/commit/5b0c46836200a57577bbd4aaa5cd463089a3bff0)] Removed `KlasaClient.sharded` as `Client.shard` is now fixed. (bdistin) @@ -160,6 +183,15 @@ NOTE: For the contributors, you add new entries to this document following this ### Fixed +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Fixed `Util.deepClone` not cloning full objects. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Fixed `SchemaFolder#_shardSyncSchema` not passing the action as string. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Fixed `null` values in *updateMany*'s pattern not updating nested keys plus individual queries. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Fixed update/reset methods in Configuration not emitting `configEntryCreate` when the entry does not exist. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Fixed the updateMany pattern in Configuration not accepting a guild. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Fixed the **configUpdateEntry** event (used to sync configuration instances across shards) running in non-sharded bots, now it will be disabled if the bot is not sharded. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Fixed `Configuration._patch` not patching after the second nested folder. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Fixed SettingResolver's return types. (kyranet) +- [[#179](https://github.com/dirigeants/klasa/pull/179)] Fixed Gateway syncing keys even when it's unused. (kyranet) - [[#184](https://github.com/dirigeants/klasa/pull/184)] Fixed classes and options missing methods and properties in typings. (kyranet) - [[#184](https://github.com/dirigeants/klasa/pull/184)] Fixed `Provider` not having abstract methods in typings. (kyranet) - [[#184](https://github.com/dirigeants/klasa/pull/184)] Fixed typings using `Store` without generic parameters, causing the TypeScript compiler to fail. (kyranet) diff --git a/guides/.docconfig.json b/guides/.docconfig.json index aa63538bc1..62b680bb84 100644 --- a/guides/.docconfig.json +++ b/guides/.docconfig.json @@ -79,6 +79,25 @@ "path": "CommandsCustomResolvers.md" }] }, +{ + "name": "Advanced SettingGateway", + "files": [{ + "name": "Understanding SchemaFolders", + "path": "UnderstandingSchemaFolders.md" + }, + { + "name": "Understanding SchemaPieces", + "path": "UnderstandingSchemaPieces.md" + }, + { + "name": "SettingGateway's Types", + "path": "SettingGatewayKeyTypes.md" + }, + { + "name": "Updating your Configuration", + "path": "SettingGatewayConfigurationUpdate.md" + }] +}, { "name": "Other Subjects", "files": [{ diff --git a/guides/Advanced SettingGateway/SettingGatewayConfigurationUpdate.md b/guides/Advanced SettingGateway/SettingGatewayConfigurationUpdate.md new file mode 100644 index 0000000000..ef502ef960 --- /dev/null +++ b/guides/Advanced SettingGateway/SettingGatewayConfigurationUpdate.md @@ -0,0 +1,38 @@ +# Updating your configuration + +Once we have our schema done with all the keys, folders and types needed, we may want to update our configuration via SettingGateway, all of this is done via {@link Configuration#update}. However, how can I update it? Use any of the following code snippets: + +```javascript +// Updating the value of a key +// This key is contained in the roles folder, and the second value is a role id, we also need +// to pass a GuildResolvable. +msg.guild.configs.update('roles.administrator', '339943234405007361', msg.guild); + +// Updating an array +// userBlacklist, as mentioned in another tutorial, it's a piece with an array of users. Using +// the following code will add or remove it, depending on the existence of the key in the configuration. +msg.guild.configs.update('userBlacklist', '272689325521502208'); + +// Ensuring the function call adds (error if it exists) +msg.guild.configs.update('userBlacklist', '272689325521502208', { action: 'add' }); + +// Ensuring the function call removes (error if it doesn't exist) +msg.guild.configs.update('userBlacklist', '272689325521502208', { action: 'remove' }); + +// Updating it with a json object +// It's the same as the first code snippet. However, this pattern is slower. +msg.guild.configs.update({ roles: { administrator: '339943234405007361' } }, msg.guild); + +// Updating multiple keys (only possible with json object) +msg.guild.configs.update({ prefix: 'k!', language: 'es-ES' }); +``` + +> **Note**: Some types require a Guild instance to work, for example, *channels*, *roles* and *members*. + +> Additionally, if no 'action' option is passed to {@link ConfigurationUpdateOptions}, it'll assume the `auto` mode, which will add or remove depending on the existence of the key. + +## Further Reading: + +- {@tutorial UnderstandingSchemaPieces} +- {@tutorial UnderstandingSchemaFolders} +- {@tutorial SettingGatewayKeyTypes} diff --git a/guides/Advanced SettingGateway/SettingGatewayKeyTypes.md b/guides/Advanced SettingGateway/SettingGatewayKeyTypes.md new file mode 100644 index 0000000000..bb59b9a2b8 --- /dev/null +++ b/guides/Advanced SettingGateway/SettingGatewayKeyTypes.md @@ -0,0 +1,66 @@ +# SettingGateway's Types + +By default, there are several built-in types that the developer can use, and with the possibility to add custom types via {@link Extendable}s as explained below. The built-in types are: + +| Name | Type | Description | +| ------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| **any** | Anything, no type restriction | Resolves anything, even objects. The usage of this type will make a key unconfigurable | +| **boolean** | A {@link Boolean} resolvable | Resolves a boolean primitive value | +| **categorychannel** | A {@link external:CategoryChannel} instance or id | Resolves a CategoryChannel | +| **channel** | A {@link external:Channel} instance or id | Resolves a channel. Be careful with using this, as it accepts any type of channel | +| **command** | A {@link Command} instance or name | Resolves a Command | +| **emoji** | An {@link external:Emoji} instance or name | Resolves a custom emoji | +| **float** | A floating point number | Resolves a [float](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) | +| **guild** | A {@link KlasaGuild} instance or id | Resolves a KlasaGuild (which extends Guild) | +| **integer** | An integer number | Resolves an [integer](https://en.wikipedia.org/wiki/Integer) number | +| **language** | A {@link Language} instance or name | Resolves a language | +| **member** | A {@link external:GuildMember} instance or id | Resolves a GuildMember | +| **msg** | A {@link KlasaMessage} instance or id | Resolves a KlasaMessage (which extends Message) | +| **role** | A {@link external:Role} instance or id | Resolves a Role | +| **string** | A {@link external:StringResolvable} | Resolves a string | +| **textchannel** | A {@link external:TextChannel} instance or id | Resolves a TextChannel | +| **url** | An URL resolvable | Resolves a URL with Node.js' URL parser | +| **user** | A {@link KlasaUser} instance or id | Resolves a KlasaUser (which extends User) | +| **voicechannel** | A {@link external:VoiceChannel} instance or id | Resolves a VoiceChannel | + +## Adding new types + +To add new types, you use an {@link Extendable} extending {@link SettingResolver}. If you don't know how to create an extendable, check the following tutorial: {@tutorial CreatingExtendables}. The following extendable is a template for this: + +```javascript +const { Extendable } = require('klasa'); + +module.exports = class extends Extendable { + + constructor(...args) { + super(...args, ['SettingResolver'], { + name: 'typeName', + klasa: true + }); + } + + /** + * Resolves my custom type! + * @param {*} data The data to resolve + * @param {KlasaGuild} guild The guild to resolve for + * @param {string} name The name of the key being resolved + * @param {Object} [minMax={}] The minimum and maximum + * @param {?number} minMax.min The minimum value + * @param {?number} minMax.max The maximum value + * @returns {Promise<*>} + */ + async extend(data, guild, name, { min, max } = {}) { + // The content + return data; + } + +}; +``` + +> **Note**: If a type does not load, you can add the type name to {@link GatewayDriver#types}, but it must be before the {@link SchemaPiece}s init as they check if the type is included in that Set. + +## Further Reading: + +- {@tutorial UnderstandingSchemaPieces} +- {@tutorial UnderstandingSchemaFolders} +- {@tutorial SettingGatewayConfigurationUpdate} diff --git a/guides/Advanced SettingGateway/UnderstandingSchemaFolders.md b/guides/Advanced SettingGateway/UnderstandingSchemaFolders.md new file mode 100644 index 0000000000..2297132c3e --- /dev/null +++ b/guides/Advanced SettingGateway/UnderstandingSchemaFolders.md @@ -0,0 +1,142 @@ +# Understanding Schema + +A schema works like a diagram or a blueprint, in SettingGateway, the schema defines the keys present in the configuration for a specific gateway. This feature serves multiple purposes: + +1. Define what keys the {@link Gateway} manages and their properties. +1. Define what type the keys must hold. +1. Define the SQL schema when using a SQL database. +1. Speed up performance when iterating over keys. + +## Adding keys + +Adding keys with the schema is like adding a piece into a box, but you can also have boxes inside other boxes. That being said, you get the box you want to modify and insert the new pieces or boxes into it. The methods to achieve that are {@link SchemaFolder#add} to add pieces (keys) and boxes (folders). + +You would normally use these two methods using the following snippet: + +```javascript +// Add a new key or folder +this.client.gateways.gatewayName.schema.add(name, options, force); +``` + +The parameters are: + +- **name**: The name of the new key. If it conflicts with a pre-existent key, this will error. +- **options**: The options for the new key or folder. Check {@link SchemaFolderAddOptions}. +- **force**: Whether this change should affect all entries. It requires a lot of processing but ensures the changes are correctly applied in both the cache and database. + +You can also extend any of the three built-in {@link Gateway}s from Klasa. For example, if you want to add a new key called **modlogs** that accepts only text channels, for your guild configs, you would use the following code: + +```javascript +this.client.gateways.guilds.schema.add('modlogs', { type: 'TextChannel' }); +``` + +Where you're doing the following steps: + +1. Access to {@link KlasaClient#gateways}, type of {@link GatewayDriver}, which holds all gateways. +1. Access to the guilds' {@link Gateway}, which manages the per-guild configuration. +1. Access to the guilds' schema via {@link Gateway#schema}, which manages the gateway's schema. +1. Add a new key called **modlogs** in the root of the schema, with a type of **TextChannel**. + +And you would have a perfectly configured modlogs key in your configs. However, you can also have an array of the same type. For example, you want to have a configurable array of users blacklisted in a guild, in a key named **userBlacklist**: + +```javascript +this.client.gateways.guilds.schema.add('userBlacklist', { type: 'User', array: true }); +``` + +And now you can have access to any of them in your guild configs like in the following snippet! + +```javascript +msg.guild.configs.modlogs; +// null +msg.guild.configs.userBlacklist; +// [] +``` + +## Removing keys + +Removing keys with the schema is quite easy, as you would have access to the {@link SchemaFolder} that holds it and remove it by its name (remember that `force` is optional and defaults to `true`) using {@link SchemaFolder#remove} as in the following example: + +```javascript +this.client.gateways.gatewayName.schema.remove(name, force); +``` + +In case you have a key you do not longer use and you want to get rid of it, for example, the recently created **userBlacklist** key for guild configs, you would run the following code: + +```javascript +this.client.gateways.guilds.schema.remove('userBlacklist'); +``` + +And the property `userBlacklist` for all guild configs will be deleted. + +## Adding folders + +Folder creation is very similar to key creation, but with one key difference: it has no options for itself, but instead, it can create its children keys (just like you can add a box with other boxes and pieces, into another). You can add a new key inside a new folder in two different ways: + +### Slower + +You can create a folder, then create the keys, however, this will iterate over all entries twice: + +```javascript +async function init() { + const { schema } = this.client.gateways.guilds; + + await schema.add('channels', {}); + await schema.channels.add('modlogs', { type: 'TextChannel' }); + console.log(schema.channels.modlogs.toJSON()); + // { + // type: 'textchannel', + // array: false, + // default: null, + // min: null, + // max: null, + // configurable: true + // } +} +``` + +### Faster + +However, it's possible to create a folder with all the sub-keys (and even more nested folders) with the folder creation. + +```javascript +async function init() { + const { schema } = this.client.gateways.guilds; + + await schema.add('channels', { modlogs: { type: 'TextChannel' } }); + console.log(schema.channels.modlogs.toJSON()); + // { + // type: 'textchannel', + // array: false, + // default: null, + // min: null, + // max: null, + // configurable: true + // } +} +``` + +> **Reminder**: To access a key inside a folder in your configuration command, you use the access operator (`.`). For example: *k!conf set channels.modlogs #modlogs* + +## Removing folders + +Removing folders is the same as removing keys, check {@link SchemaFolder#remove}, the difference is that, while removing a key will remove one value from the schema, removing a folder will remove it with all its nested keys and folders, even very nested ones. + +## Ensuring the existence of a key. + +In [klasa pieces](https://github.com/dirigeants/klasa-pieces/) specially, some pieces require a key from the configuration to work, however, the creator of the pieces does not know if the user who downloads the piece has it, so this function becomes useful in this case. + +```javascript +async function init() { + const { schema } = this.client.gateways.guilds; + + if (!schema.has('modlog')) { + await schema.add('modlog', { type: 'TextChannel' }); + } +} +``` + +## Further Reading: + +- {@tutorial UnderstandingSchemaPieces} +- {@tutorial SettingGatewayKeyTypes} +- {@tutorial SettingGatewayConfigurationUpdate} diff --git a/guides/Advanced SettingGateway/UnderstandingSchemaPieces.md b/guides/Advanced SettingGateway/UnderstandingSchemaPieces.md new file mode 100644 index 0000000000..0990620ccd --- /dev/null +++ b/guides/Advanced SettingGateway/UnderstandingSchemaPieces.md @@ -0,0 +1,67 @@ +# Understanding Schema's Keys + +As mentioned in the previous tutorial, {@tutorial UnderstandingSchema}, SettingGateway's schema is divided in two parts: **folders** and **pieces**. Pieces are contained in folders, but they cannot have keys nor folders. Instead, this holds the key's metadata such as its type, if it's configurable by the configuration command... you can check more information in the documentation: {@link SchemaPiece}. + +## Key options + +There are multiple options that configure the piece, they are: + +| Option | Description | +| ------------ | -------------------------------------------------------------------------- | +| array | Whether the values should be stored in an array | +| configurable | Whether this key can be configured with the built-in configuration command | +| default | The default value for this key | +| max | The maximum value for this key, only applies for string and numbers | +| min | The minimum value for this key, only applies for string and numbers | +| sql | The SQL datatype for this key | +| type | The type for this key | + +> Check {@tutorial SettingGatewayKeyTypes} for the supported types and how to extend them. + +## Default option + +*The default option is optional, but, what is its default value?* + +The default option is one of the last options to default, **array** defaults to `false`, **max** and **min** defaults to `null`, **configurable** defaults to either `true` or `false`, the latter if **type** is `any`; and **type** is always obligatory. + +- If **array** is true, default will be an empty array: `[]`. +- If **type** is boolean, default will be `false`. +- In any other case, it will be `null`. + +After default, the sql type is calculated with a valid datatype. Keep in mind that it uses standard SQL types, but may work better for PostgreSQL. In any case, if you want to use a type that is very specific to your database, consider including this option. + +## Editing key options + +Once created, it's possible since 0.5.0 to edit a {@link SchemaPiece}'s options, it's as simple as running {@link SchemaPiece#edit} which takes the same options for adding a key with {@link SchemaFolder#addKey} but with one exception: `array` and `type` can't change. The syntax is the following: + +```javascript +this.client.gateways.gatewayName.schema.keyName.edit(options); +``` + +For example, let's say we dislike the current prefix and we want to change it to `s!` for the next entries, then you can simply do: + +```javascript +this.client.gateways.guilds.schema.prefix.edit({ default: 's!' }); +``` + +Where you're doing the following steps: + +1. Access to {@link KlasaClient#gateways}, type of {@link GatewayDriver}, which holds all gateways. +1. Access to the guilds' {@link Gateway}, which manages the per-guild configuration. +1. Access to the guilds' schema via {@link Gateway#schema}, which manages the gateway's schema. +1. Access to the key we want to edit, in this case, the **prefix** key, which is type of {@link SchemaPiece}. +1. Call {@link SchemaPiece#edit} with the option `default` and the new value: `'s!'`. + +### The Type Issue + +The main reason for why we don't support editing the options `array` and `type` is: + +> Changing the type is very complex. For example, in SQL, if we changed the type from `TEXT`, `VARCHAR`, or any other string type to a numeric one such as `INTEGER`, we could risk the database potentially throwing an error or setting them to null, which would result in data loss. We would then need to download all of the data first, and insert them back with the new type. The same thing happens in NoSQL. + +Changing the value of `array` from a non-string datatype can result on the issue above, and it's a very slow process. Therefore, it's much better to just remove the key and add it back. + +## Further Reading: + +- {@tutorial UnderstandingSchemaFolders} +- {@tutorial SettingGatewayKeyTypes} +- {@tutorial SettingGatewayConfigurationUpdate} diff --git a/guides/Getting Started/UnderstandingSettingGateway.md b/guides/Getting Started/UnderstandingSettingGateway.md index 0fb1320e71..ddd1fac7e7 100644 --- a/guides/Getting Started/UnderstandingSettingGateway.md +++ b/guides/Getting Started/UnderstandingSettingGateway.md @@ -1,215 +1,50 @@ # SettingGateway -The SettingGateway is designed to provide users a very useful interface for managing data. Each instance is able to handle a completely different schema and database. +What is SettingGateway? It is an interface that connects your Discord bot with a database and ensures maximum performance by using a very refined cache system that is always up to date with the database. In a point of view, SettingGateway can be understood as an abstracted [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) as it's able to run any kind database (with a compatible {@link Provider}) and manage the data efficiently. -By default, Klasa uses the [json](https://github.com/dirigeants/klasa/blob/master/src/providers/json.js) provider. Do not be fooled and insta-replace with SQLite, Klasa's JSON provider writes the data [atomically](https://en.wikipedia.org/wiki/Atomicity_(database_systems) ), in other words, it is very rare for the data to corrupt. +By default, Klasa uses the [json](https://github.com/dirigeants/klasa/blob/master/src/providers/json.js) provider. Do not be fooled and insta-replace with SQLite, Klasa's JSON provider writes the data [atomically](https://en.wikipedia.org/wiki/Atomicity_%28database_systems%29). In other words, it is very rare for the data to corrupt. -## Key types +Thanks to the abstraction of SettingGateway, the developer has many options, for example, if you want to change the database that manages the data, you just change one line of code, without needing to rewrite everything that relies on it, nor you need to rewrite the interface itself in order to be able to work with a different database. -The types supported for Gateway's keys are the same as the name of the properties from the {@link SettingResolver}, by extending it, you'll also extend the available key types. +## Database Engine -| Name | Type | Description | -| ---------------- | ------------------------------------- | ---------------------------------------------------------------------------------------- | -| **any** | Anything, no type restriction | Resolves anything, even objects, the usage of this type will make a key unconfigurable | -| **boolean** | A boolean resolvable | Resolves a boolean primitive value | -| **channel** | A {@link Channel} instance or id | Resolves a channel. Be careful with using this, as it accepts any type of channel | -| **command** | A {@link Command} instance or name | Resolves a Command | -| **emoji** | An {@link Emoji} instance or name | Resolves a custom emoji | -| **float** | A floating point number | Resolves a [float](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) | -| **guild** | A {@link KlasaGuild} instance or id | Resolves a KlasaGuild (which extends Guild) | -| **integer** | An integer number | Resolves an [integer](https://en.wikipedia.org/wiki/Integer) number | -| **language** | A {@link Language} instance or name | Resolves a language | -| **member** | A {@link GuildMember} instance or id | Resolves a GuildMember | -| **msg** | A {@link KlasaMessage} instance or id | Resolves a KlasaMessage (which extends Message) | -| **role** | A {@link Role} instance or id | Resolves a Role | -| **string** | A string resolvable | Resolves a string | -| **textchannel** | A {@link TextChannel} instance or id | Resolves a TextChannel | -| **url** | An URL resolvable | Resolves a URL with Node.js' URL parser | -| **user** | A {@link KlasaUser} instance or id | Resolves a KlasaUser (which extends User) | -| **voicechannel** | A {@link VoiceChannel} instance or id | Resolves a VoiceChannel | +As mentioned before, SettingGateway is abstracted, it does not rely on a very specific database, but can use any of them. In a production bot, you may want to use a process-based database such as rethinkdb, mongodb or postgresql, you can check and download them from the [klasa-pieces](https://github.com/dirigeants/klasa-pieces/) repository so you don't need to make one from scratch. -> To extend the types, you may extend {@link SettingResolver} by making an extendable, check how to make them here: {@tutorial CreatingExtendables}. - -## Change the *provider's engine*. - -For example, let's say I have downloaded the *rethinkdb* provider and I want to work with it, then we go to your main script file (`app.js`, `bot.js`..., wherever you declare the new Klasa.Client), and write the following code: +Now... how do we update it? Go to your main file, where {@link KlasaClient} is initialized, and add a new option to your {@link KlasaClientOptions}. The following code snippet as an example: ```javascript -const client = new Klasa.Client({ providers: { default: 'rethinkdb' } }); +const client = new KlasaClient({ providers: { default: 'rethinkdb' } }); ``` -Your Klasa's configuration will look something like this: +If you have other options, such as a prefix, then your main file would look like this: ```javascript -const client = new Klasa.Client({ +const { KlasaClient } = require('klasa'); + +new KlasaClient({ prefix: 'k!', providers: { default: 'rethinkdb' } -}); - -client.login('A_BEAUTIFUL_TOKEN_AINT_IT?'); +}).login('A_BEAUTIFUL_TOKEN_AINT_IT?'); ``` And now, you're using rethinkdb's provider to store the data from SettingGateway. -What happens when I use an engine that does not exist as a provider? Simple, SettingGateway will throw an error. Should this happen, make sure you typed the provider's name correctly. - -## Add new 'keys' to the guild configuration's schema. - -You can easily add keys to the schema by doing this: - -```javascript -this.client.gateways.guilds.schema.addKey(key, options, force); -``` - -Where: - -- `key` is the key's name to add, `String` type. -- `options` is an object containing the options for the key, such as `type`, `default`, `sql`, `array`... -- `force` (defaults to `true`) is whether SchemaManager should update all documents/rows to match the new schema, using the `options.default` value. - -For example, let's say I want to add a new configuration key, called `modlogs`, which takes a channel. - -```javascript -this.client.gateways.guilds.schema.addKey('modlogs', { type: 'TextChannel' }); -``` - -This will create a new configuration key, called `modlogs`, and will take a `TextChannel` type. - -> The force parameter defaults to `true` instead of `false`. It's recommended to leave it as true to avoid certain unwanted actions. - -But now, I want to add another key, with name of `users`, *so I can set a list of blacklisted users who won't be able to use commands*, which will take an array of Users. - -```javascript -this.client.gateways.guilds.schema.addKey('users', { type: 'User', array: true }); -``` - -> `options.array` defaults to `false`, and when `options.default` is not specified, it defaults to `null`, however, when `options.array` is `true`, `options.default` defaults to `[]` (empty array). - -What have we done? `client.gateways.guilds.schema` is a {@link SchemaFolder} instance (also called Folder type) which can manage itself, such as adding keys/folders to itself (it certainly follows the OOP paradigm). - -## Editing keys from the guild configuration. - -Now that I have a new key called `modlogs`, I want to configure it outside the `conf` command, how can we do this? - -```javascript -msg.guild.configs.update('modlogs', '267727088465739778', msg.guild); -``` - -Check: {@link Configuration#update} - -> You can use a Channel instance, {@link SettingResolver} will make sure the input is valid and the database gets an **ID** and not an object. - -Now, I want to **add** a new user user to the `users` key, which takes an array. - -```javascript -msg.guild.configs.update('users', '146048938242211840', { action: 'add' }); -``` - -That will add the user `'146048938242211840'` to the `users` array. To remove it: - -```javascript -msg.guild.configs.update('users', '146048938242211840', { action: 'remove' }); -``` - -> Additionally, if no 'action' option is passed to {@link Configuration.ConfigurationUpdateOptions}, it'll assume the `auto` mode, which will add or remove depending on the existence of the key. - -## Removing a key from the guild configuration. - -I have a key which is useless for me, so I *want* to remove it from the schema. - -```javascript -this.client.gateways.guilds.schema.removeKey('users'); -``` - -## Create a new folder in the schema - -It's very similar to how you create a new key, but it only accepts three arguments: - -```javascript -this.client.gateways.guilds.schema.addFolder(name, object, force); -``` - -So, let's say I want a key called 'modlogs' into the 'channels' folder for organization. There are two ways to do it: - -### Slower +## Creating Gateways -```javascript -async () => { - const { schema } = this.client.gateways.guilds; - - await schema.addFolder('channels'); - await schema.channels.addKey('modlogs', { type: 'TextChannel' }); - console.log(schema.channels.modlogs.toJSON()); - // { - // type: 'textchannel', - // array: false, - // default: null, - // min: null, - // max: null, - // configurable: true - // } -}; -``` +Another advantage of using this interface is that it can handle multiple databases simultaneously, for example, Klasa handles 3 gateways at the same time: `clientStorage` for Client, `guilds` for Guild and `users` for User. Plus, there's the possibility to add a new {@link Gateway} by using {@link KlasaClient#gateways}: -### Faster +Let's say I want to add a new Gateway instance called `channels` that stores data to complement our permissions, and I want the **postgresql** provider to handle it. ```javascript -async () => { - const { schema } = this.client.gateways.guilds; - - await schema.addFolder('channels', { modlogs: { type: 'TextChannel' } }); - console.log(schema.channels.modlogs.toJSON()); - // { - // type: 'textchannel', - // array: false, - // default: null, - // min: null, - // max: null, - // configurable: true - // } -}; -``` - -Now, how we do configure it with the built-in conf command? Easy: - -```sh -k!conf set channels.modlogs #modlogs -``` - -## Add a key to the guild configuration's schema if it doesn't exist. +const { KlasaClient } = require('klasa'); -In [Klasa-Pieces](https://github.com/dirigeants/klasa-pieces/), specially, some pieces require a key from the configuration to work, however, the creator of the pieces does not know if the user who downloads the piece has it, so this function becomes useful in this case. - -```javascript -async () => { - const { schema } = this.client.gateways.guilds; - - if (!schema.hasKey('modlog')) { - await schema.addKey('modlog', { type: 'TextChannel' }); - } -}; -``` - -## How can I create new Gateway instances? - -By using {@link GatewayDriver}, (available from `client.gateways`). - -Let's say I want to add a new Gateway instance called `channels` that stores data to complement our permissions. -You'll want a validate function to ensure what you're inputting is a valid channel, and you would want a channel specific schema to handle our channel specific permissions, like the two below. - -```javascript -// Must use the function keyword or be a method of a class. -async function validate(channelResolvable) { - // 'this' is referred to the GatewayDriver's instance, it has access - // to client, resolver... - const result = await this.resolver.channel(channelResolvable); - if (result) return result; - - throw 'The parameter expects either a Channel ID or a Channel Instance.'; -} +const client = new KlasaClient({ + prefix: 'k!', + providers: { default: 'rethinkdb' } +}); -// Define the schema for the new Gateway. -const schema = { +// Now, we create it: +client.gateways.register('channels', { disabledCommands: { type: 'Command', default: [], @@ -227,67 +62,39 @@ const schema = { min: 0, max: 30 } -}; +}, { provider: 'postgresql' }); -// Now, we create it: -this.client.gateways.add('channels', validate, schema); +client.login('A_BEAUTIFUL_TOKEN_AINT_IT?'); ``` -> Since [[#43](https://github.com/dirigeants/klasa/pull/43)], validate only accepts a single argument, instead of resolver being the first one. - -> The `validate` function must be a [**function**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function), not a [**Arrow Function**](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions), the difference between them is that an arrow function binds `this` to wherever the function has been created (for example, the `exports` from your eval command, if you are doing this with eval), while the normal functions does not do this. - -> If the `validate` function does not resolve **Guild** type, you might want to use the third argument of {@link Configuration#update}, which takes a Guild resolvable. +> **Note**: You can have any schema, check the links below to understand how to expand it later. And then, you can access to it by: ```javascript -this.client.gateways.channels; -``` - -## Using different providers in different gateways - -This is new from the SettingGateway v2 (check [#43](https://github.com/dirigeants/klasa/pull/43)), when creating a new Gateway (check above for how to do it), there's an extra parameter in `client.gateways.add` called `options`. It's optional, but it accepts an object with one key: `provider`, which is the Provider/SQLProvider (json, leveldb, rethinkdb...). For example: - -```javascript -this.client.gateways.add('channels', validate, schema, { provider: 'rethinkdb' }); +client.gateways.channels; ``` -The code above will create a new Gateway instance called 'channels', which will use RethinkDB to store the persistent data. - ## Customizing the options for each built-in gateway -This is available in 0.5.0 since the PR [#152](https://github.com/dirigeants/klasa/pull/152), and you're able to configure the three built-in gateways: `guilds`, `users` and `clientStorage`. The option to configure them is {@link KlasaClient.KlasaClientOptions.gateways}, where you would add the option `gateways` to your KlasaClientOptions: +This is available in 0.5.0 since the PR [#152](https://github.com/dirigeants/klasa/pull/152), and you're able to configure the three built-in gateways: `guilds`, `users` and `clientStorage`. The option to configure them is {@link KlasaClientOptions.gateways}, where you would add the option `gateways` to your KlasaClientOptions: ```javascript -const client = new Klasa.Client({ +new Klasa.Client({ prefix: 'k!', providers: { default: 'json' }, gateways: { guilds: { provider: 'rethinkdb' }, users: { provider: 'postgresql' } } -}); - -client.login('A_BEAUTIFUL_TOKEN_AINT_IT?'); +}).login('A_BEAUTIFUL_TOKEN_AINT_IT?'); ``` Where the *clientStorage* gateway would take the default options (json provider), the *guilds* gateway would use the rethinkdb provider, and finally the *users* one would use the postgresql provider. These options are {@link GatewayDriver.GatewayDriverAddOptions}. -## Modifying a SchemaPiece's parameters - -Once created, it's possible since 0.5.0 to modify a {@link SchemaPiece}'s parameter, it's as simple as doing {@link SchemaPiece#update} which takes the same options for adding a key with {@link SchemaFolder#addKey} but with one exception: `array` and `type` can't change. - -For example, let's say we dislike the current prefix and we want to change it to `s!` for the next entries, then you can simply do: - -```javascript -this.client.gateways.guilds.schema.prefix.modify({ default: 's!' }); -``` - -### The Type Issue - -The main reason for why we don't support modifying the parameters `array` and `type` is: - -> Changing the type is very complex. For example, in SQL, if we changed the type from `TEXT`, `VARCHAR`, or any other string type to a numeric one such as `INTEGER`, we could risk the database potentially throwing an error or setting them to null, which would result in data loss. We would then need to download all of the data first, and insert them back with the new type. The same thing happens in NoSQL. +## Further Reading: -Changing the value of `array` from a non-string datatype can result on the issue above, and it's a very slow process. Therefore, it's much better to just remove the key and add it back. +- {@tutorial UnderstandingSchemaPieces} +- {@tutorial UnderstandingSchemaFolders} +- {@tutorial SettingGatewayKeyTypes} +- {@tutorial SettingGatewayConfigurationUpdate} diff --git a/package.json b/package.json index 7ac74dde21..0d895aafbd 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "devDependencies": { "@types/node": "^9.4.6", "docgen": "github:dirigeants/docsgen", - "eslint": "^4.17.0", + "eslint": "^4.18.1", "eslint-config-klasa": "github:dirigeants/klasa-lint", "eslint-plugin-markdown": "^1.0.0-beta.6", "markdownlint-cli": "^0.7.1", diff --git a/src/commands/Admin/conf.js b/src/commands/Admin/conf.js index 45b18a8665..a93e4a153a 100644 --- a/src/commands/Admin/conf.js +++ b/src/commands/Admin/conf.js @@ -25,28 +25,35 @@ module.exports = class extends Command { } get(msg, [key]) { - const { path } = this.client.gateways.guilds.getPath(key, { avoidUnconfigurable: true, piece: true }); - return msg.sendMessage(msg.language.get('COMMAND_CONF_GET', path.path, path.resolveString(msg))); + const { piece } = this.client.gateways.guilds.getPath(key, { avoidUnconfigurable: true, piece: true }); + return msg.sendMessage(msg.language.get('COMMAND_CONF_GET', piece.path, msg.guild.configs.resolveString(msg, piece))); } async set(msg, [key, ...valueToSet]) { - const { path } = await msg.guild.configs.update(key, valueToSet.join(' '), msg.guild, { avoidUnconfigurable: true, action: 'add' }); - return msg.sendMessage(msg.language.get('COMMAND_CONF_UPDATED', path.path, path.resolveString(msg))); + const { errors, updated } = await msg.guild.configs.update(key, valueToSet.join(' '), msg.guild, { avoidUnconfigurable: true, action: 'add' }); + if (errors.length) return msg.sendMessage(errors[0]); + if (!updated.length) return msg.sendMessage(msg.language.get('COMMAND_CONF_NOCHANGE', key)); + return msg.sendMessage(msg.language.get('COMMAND_CONF_UPDATED', key, msg.guild.configs.resolveString(msg, updated[0].piece))); } async remove(msg, [key, ...valueToRemove]) { - const { path } = await msg.guild.configs.update(key, valueToRemove.join(' '), msg.guild, { avoidUnconfigurable: true, action: 'remove' }); - return msg.sendMessage(msg.language.get('COMMAND_CONF_UPDATED', path.path, path.resolveString(msg))); + const { errors, updated } = await msg.guild.configs.update(key, valueToRemove.join(' '), msg.guild, { avoidUnconfigurable: true, action: 'remove' }); + if (errors.length) return msg.sendMessage(errors[0]); + if (!updated.length) return msg.sendMessage(msg.language.get('COMMAND_CONF_NOCHANGE', key)); + return msg.sendMessage(msg.language.get('COMMAND_CONF_UPDATED', key, msg.guild.configs.resolveString(msg, updated[0].piece))); } async reset(msg, [key]) { - const { path } = await msg.guild.configs.reset(key, true); - return msg.sendMessage(msg.language.get('COMMAND_CONF_RESET', path.path, path.resolveString(msg))); + const { errors, updated } = await msg.guild.configs.reset(key, msg.guild, true); + if (errors.length) return msg.sendMessage(errors[0]); + if (!updated.length) return msg.sendMessage(msg.language.get('COMMAND_CONF_NOCHANGE', key)); + return msg.sendMessage(msg.language.get('COMMAND_CONF_RESET', key, msg.guild.configs.resolveString(msg, updated[0].piece))); } list(msg, [key]) { - const { path } = this.client.gateways.guilds.getPath(key, { avoidUnconfigurable: true, piece: false }); - return msg.sendMessage(msg.language.get('COMMAND_CONF_SERVER', key ? `: ${key.split('.').map(toTitleCase).join('/')}` : '', codeBlock('asciidoc', path.getList(msg)))); + const { piece } = this.client.gateways.guilds.getPath(key, { avoidUnconfigurable: true, piece: false }); + return msg.sendMessage(msg.language.get('COMMAND_CONF_SERVER', key ? `: ${key.split('.').map(toTitleCase).join('/')}` : '', + codeBlock('asciidoc', msg.guild.configs.list(msg, piece)))); } }; diff --git a/src/commands/General/User Configs/userconf.js b/src/commands/General/User Configs/userconf.js index a01674e85d..092a89d49c 100644 --- a/src/commands/General/User Configs/userconf.js +++ b/src/commands/General/User Configs/userconf.js @@ -23,28 +23,35 @@ module.exports = class extends Command { } get(msg, [key]) { - const { path } = this.client.gateways.users.getPath(key, { avoidUnconfigurable: true, piece: true }); - return msg.sendMessage(msg.language.get('COMMAND_CONF_GET', path.path, path.resolveString(msg))); + const { piece } = this.client.gateways.users.getPath(key, { avoidUnconfigurable: true, piece: true }); + return msg.sendMessage(msg.language.get('COMMAND_CONF_GET', piece.path, msg.author.configs.resolveString(msg, piece))); } async set(msg, [key, ...valueToSet]) { - const { path } = await msg.author.configs.update(key, valueToSet.join(' '), msg.guild, { avoidUnconfigurable: true, action: 'add' }); - return msg.sendMessage(msg.language.get('COMMAND_CONF_UPDATED', path.path, path.resolveString(msg))); + const { errors, updated } = await msg.author.configs.update(key, valueToSet.join(' '), msg.guild, { avoidUnconfigurable: true, action: 'add' }); + if (errors.length) return msg.sendMessage(errors[0]); + if (!updated.length) return msg.sendMessage(msg.language.get('COMMAND_CONF_NOCHANGE', key)); + return msg.sendMessage(msg.language.get('COMMAND_CONF_UPDATED', key, msg.author.configs.resolveString(msg, updated[0].piece))); } async remove(msg, [key, ...valueToRemove]) { - const { path } = await msg.author.configs.update(key, valueToRemove.join(' '), msg.guild, { avoidUnconfigurable: true, action: 'remove' }); - return msg.sendMessage(msg.language.get('COMMAND_CONF_UPDATED', path.path, path.resolveString(msg))); + const { errors, updated } = await msg.author.configs.update(key, valueToRemove.join(' '), msg.guild, { avoidUnconfigurable: true, action: 'remove' }); + if (errors.length) return msg.sendMessage(errors[0]); + if (!updated.length) return msg.sendMessage(msg.language.get('COMMAND_CONF_NOCHANGE', key)); + return msg.sendMessage(msg.language.get('COMMAND_CONF_UPDATED', key, msg.author.configs.resolveString(msg, updated[0].piece))); } async reset(msg, [key]) { - const { path } = await msg.author.configs.reset(key, true); - return msg.sendMessage(msg.language.get('COMMAND_CONF_RESET', path.path, path.resolveString(msg))); + const { errors, updated } = await msg.author.configs.reset(key, true); + if (errors.length) return msg.sendMessage(errors[0]); + if (!updated.length) return msg.sendMessage(msg.language.get('COMMAND_CONF_NOCHANGE', key)); + return msg.sendMessage(msg.language.get('COMMAND_CONF_RESET', key, msg.author.configs.resolveString(msg, updated[0].piece))); } list(msg, [key]) { - const { path } = this.client.gateways.users.getPath(key, { avoidUnconfigurable: true, piece: false }); - return msg.sendMessage(msg.language.get('COMMAND_CONF_USER', key ? `: ${key.split('.').map(toTitleCase).join('/')}` : '', codeBlock('asciidoc', path.getList(msg)))); + const { piece } = this.client.gateways.users.getPath(key, { avoidUnconfigurable: true, piece: false }); + return msg.sendMessage(msg.language.get('COMMAND_CONF_USER', key ? `: ${key.split('.').map(toTitleCase).join('/')}` : '', + codeBlock('asciidoc', msg.author.configs.list(msg, piece)))); } }; diff --git a/src/events/configUpdateEntry.js b/src/events/configUpdateEntry.js index ad36b5a139..ceff85223d 100644 --- a/src/events/configUpdateEntry.js +++ b/src/events/configUpdateEntry.js @@ -4,14 +4,14 @@ module.exports = class extends Event { run(configs) { if (!this.client.shard) return; - if (configs.type === 'users') { + if (configs.gateway.type === 'users') { this.client.shard.broadcastEval(` if (this.shard.id !== ${this.client.shard.id}) { const user = this.users.get('${configs.id}'); if (user) user.configs.sync(); } `); - } else if (configs.type === 'clientStorage') { + } else if (configs.gateway.type === 'clientStorage') { this.client.shard.broadcastEval(` if (this.shard.id !== ${this.client.shard.id}) { this.configs.sync(); @@ -20,4 +20,8 @@ module.exports = class extends Event { } } + init() { + if (!this.client.shard) this.disable(); + } + }; diff --git a/src/languages/en-US.js b/src/languages/en-US.js index 6bf0cc649f..60eccb872a 100644 --- a/src/languages/en-US.js +++ b/src/languages/en-US.js @@ -143,6 +143,7 @@ module.exports = class extends Language { COMMAND_CONF_GET_NOEXT: (key) => `The key **${key}** does not seem to exist.`, COMMAND_CONF_GET: (key, value) => `The value for the key **${key}** is: \`${value}\``, COMMAND_CONF_RESET: (key, response) => `The key **${key}** has been reset to: \`${response}\``, + COMMAND_CONF_NOCHANGE: (key) => `The value for **${key}** was already that value.`, COMMAND_CONF_SERVER_DESCRIPTION: 'Define per-server configuration.', COMMAND_CONF_SERVER: (key, list) => `**Server Configuration${key}**\n${list}`, COMMAND_CONF_USER_DESCRIPTION: 'Define per-user configuration.', diff --git a/src/lib/Client.js b/src/lib/Client.js index 37500ce598..a1b9cfcb3e 100644 --- a/src/lib/Client.js +++ b/src/lib/Client.js @@ -58,19 +58,16 @@ class KlasaClient extends Discord.Client { * @property {(string|Function)} [readyMessage=`Successfully initialized. Ready to serve ${this.guilds.size} guilds.`] readyMessage to be passed throughout Klasa's ready event * @property {RegExp} [regexPrefix] The regular expression prefix if one is provided * @property {boolean} [typing=false] Whether the bot should type while processing commands - * @memberof KlasaClient */ /** * @typedef {Object} KlasaProvidersOptions * @property {string} [default] The default provider to use - * @memberof KlasaClient */ /** * @typedef {Object} KlasaClientOptionsClock * @property {number} [interval] The interval in milliseconds for the clock to check the tasks - * @memberof KlasaClient */ /** @@ -78,7 +75,6 @@ class KlasaClient extends Discord.Client { * @property {GatewayDriverAddOptions} [clientStorage] The options for clientStorage's gateway * @property {GatewayDriverAddOptions} [guilds] The options for guilds' gateway * @property {GatewayDriverAddOptions} [users] The options for users' gateway - * @memberof KlasaClient */ /** @@ -88,7 +84,6 @@ class KlasaClient extends Discord.Client { * @property {boolean} [useColor=false] Whether the client console should use colors * @property {Colors} [colors] Color formats to use * @property {(boolean|string)} [timestamps=true] Whether to use timestamps or not, or the Timestamp format of the timestamp you want to use - * @memberof KlasaClient */ /** @@ -99,7 +94,6 @@ class KlasaClient extends Discord.Client { * @property {boolean} [verbose=false] If the verbose event should be enabled by default * @property {boolean} [warn=true] If the warn event should be enabled by default * @property {boolean} [wtf=true] If the wtf event should be enabled by default - * @memberof KlasaClient */ /** @@ -112,7 +106,6 @@ class KlasaClient extends Discord.Client { * @property {LanguageOptions} [languages={}] The default language options * @property {MonitorOptions} [monitors={}] The default monitor options * @property {ProviderOptions} [providers={}] The default provider options - * @memberof KlasaClient */ /** @@ -120,15 +113,6 @@ class KlasaClient extends Discord.Client { * @property {number} [promptLimit=Infinity] The number of re-prompts before custom prompt gives up * @property {number} [promptTime=30000] The time-limit for re-prompting custom prompts * @property {boolean} [quotedStringSupport=false] Whether the custom prompt should respect quoted strings - * @memberof KlasaClient - */ - - /** - * @typedef {Object} ConfigUpdateEntryMany - * @property {'MANY'} type The type for config updates made with the updateMany pattern - * @property {string[]} keys The keys changed - * @property {Array<*>} values The values changed - * @memberof KlasaClient */ /** @@ -141,6 +125,13 @@ class KlasaClient extends Discord.Client { config = util.mergeDefault(constants.DEFAULTS.CLIENT, config); super(config); + /** + * The options the client was instantiated with. + * @since 0.5.0 + * @name KlasaClient#options + * @type {KlasaClientOptions} + */ + /** * The directory to the node_modules folder where Klasa exists * @since 0.0.1 @@ -410,7 +401,7 @@ class KlasaClient extends Discord.Client { * Use this to login to Discord with your bot * @since 0.0.1 * @param {string} token Your bot token - * @returns {Promise} + * @returns {string} */ async login(token) { const timer = new Stopwatch(); @@ -426,19 +417,20 @@ class KlasaClient extends Discord.Client { // Add the gateways await Promise.all([ - this.gateways.add('guilds', constants.GATEWAY_RESOLVERS.GUILDS, this.gateways.guildsSchema, this.options.gateways.guilds, false), - this.gateways.add('users', constants.GATEWAY_RESOLVERS.USERS, undefined, this.options.gateways.users, false), - this.gateways.add('clientStorage', constants.GATEWAY_RESOLVERS.CLIENT_STORAGE, this.gateways.clientStorageSchema, this.options.gateways.clientStorage, false) + this.gateways.add('guilds', this.gateways.guildsSchema, this.options.gateways.guilds, false), + this.gateways.add('users', undefined, this.options.gateways.users, false), + this.gateways.add('clientStorage', this.gateways.clientStorageSchema, this.options.gateways.clientStorage, false) ]); // Automatic Prefix editing detection. if (typeof this.options.prefix === 'string' && this.options.prefix !== this.gateways.guilds.schema.prefix.default) { - await this.gateways.guilds.schema.prefix.modify({ default: this.options.prefix }); + await this.gateways.guilds.schema.prefix.edit({ default: this.options.prefix }); } - if (this.gateways.guilds.schema.hasKey('disabledCommands')) { + if (this.gateways.guilds.schema.has('disabledCommands')) { const languageStore = this.languages; + const commandStore = this.commands; this.gateways.guilds.schema.disabledCommands.setValidator(function (command, guild) { // eslint-disable-line - if (command && command.guarded) throw (guild ? guild.language : languageStore.default).language.get('COMMAND_CONF_GUARDED', command.name); + if ((cmd => cmd && cmd.guarded)(commandStore.get(command))) throw (guild ? guild.language : languageStore.default).get('COMMAND_CONF_GUARDED', command); }); } @@ -457,8 +449,8 @@ class KlasaClient extends Discord.Client { if (!this.options.ownerID) this.options.ownerID = this.user.bot ? this.application.owner.id : this.user.id; // Client-wide settings - this.configs = this.gateways.clientStorage.cache.get('clientStorage', this.user.id) || this.gateways.clientStorage.insertEntry(this.user.id); - await this.configs.sync().then(() => this.gateways.clientStorage.cache.set(this.type, this.user.id, this.configs)); + this.configs = this.gateways.clientStorage.cache.get(this.user.id) || this.gateways.clientStorage.insertEntry(this.user.id); + await this.configs.sync(); // Init all the pieces await Promise.all(this.pieceStores.filter(store => !['providers', 'extendables'].includes(store.name)).map(store => store.init())); @@ -645,25 +637,24 @@ KlasaClient.defaultPermissionLevels = new PermLevels() */ /** - * Emitted when {@link Configuration.update} is run, the parameter path will be an object with the following format: - * `{ type: 'MANY', keys: string[], values: Array<*> }` + * Emitted when {@link Configuration#update} or {@link Configuration#reset} is run. * @event KlasaClient#configUpdateEntry * @since 0.5.0 * @param {Configuration} oldEntry The old configuration entry * @param {Configuration} newEntry The new configuration entry - * @param {(string|ConfigUpdateEntryMany)} path The path of the key which changed + * @param {ConfigurationUpdateResultEntry[]} path The path of the key which changed */ /** - * Emitted when {@link Gateway.deleteEntry} is run. + * Emitted when {@link Gateway#deleteEntry} is run. * @event KlasaClient#configDeleteEntry * @since 0.5.0 * @param {Configuration} entry The entry which got deleted */ /** - * Emitted when {@link Gateway.createEntry} is run or when {@link Gateway.getEntry} - * with the create parameter set to true creates the entry. + * Emitted when {@link Gateway#createEntry} is run, when {@link Gateway#getEntry} + * with the create parameter set to true creates the entry, or an entry with no persistence gets updated. * @event KlasaClient#configCreateEntry * @since 0.5.0 * @param {Configuration} entry The entry which got created diff --git a/src/lib/extensions/KlasaGuild.js b/src/lib/extensions/KlasaGuild.js index e42e3c7336..f6a44fc9ac 100644 --- a/src/lib/extensions/KlasaGuild.js +++ b/src/lib/extensions/KlasaGuild.js @@ -18,7 +18,7 @@ module.exports = Structures.extend('Guild', Guild => { * @since 0.5.0 * @type {Configuration} */ - this.configs = this.client.gateways.guilds.cache.get('guilds', this.id) || this.client.gateways.guilds.insertEntry(this.id); + this.configs = this.client.gateways.guilds.cache.get(this.id) || this.client.gateways.guilds.insertEntry(this.id); } /** diff --git a/src/lib/extensions/KlasaMessage.js b/src/lib/extensions/KlasaMessage.js index 0ee82ac846..fab1930845 100644 --- a/src/lib/extensions/KlasaMessage.js +++ b/src/lib/extensions/KlasaMessage.js @@ -113,7 +113,7 @@ module.exports = Structures.extend('Message', Message => { * Awaits a response from the author. * @param {string} text The text to prompt the author * @param {number} [time=30000] The time to wait before giving up - * @returns {Promise} + * @returns {KlasaMessage} */ async prompt(text, time = 30000) { const message = await this.channel.send(text); @@ -126,7 +126,7 @@ module.exports = Structures.extend('Message', Message => { /** * The usable commands by the author in this message's context * @since 0.0.1 - * @returns {Promise>} The filtered CommandStore + * @returns {Collection} The filtered CommandStore */ async usableCommands() { const col = new Collection(); @@ -144,7 +144,7 @@ module.exports = Structures.extend('Message', Message => { * Checks if the author of this message, has applicable permission in this message's context of at least min * @since 0.0.1 * @param {number} min The minimum level required - * @returns {Promise} + * @returns {boolean} */ async hasAtLeastPermissionLevel(min) { const { permission } = await this.client.permissionLevels.run(this, min); diff --git a/src/lib/extensions/KlasaUser.js b/src/lib/extensions/KlasaUser.js index 334ad8b8d8..ec60fa8550 100644 --- a/src/lib/extensions/KlasaUser.js +++ b/src/lib/extensions/KlasaUser.js @@ -14,11 +14,11 @@ module.exports = Structures.extend('User', User => { super(...args); /** - * The guild level configs for this context (guild || default) + * The user level configs for this context (user || default) * @since 0.5.0 * @type {Configuration} */ - this.configs = this.client.gateways.users.cache.get('users', this.id) || this.client.gateways.users.insertEntry(this.id); + this.configs = this.client.gateways.users.cache.get(this.id) || this.client.gateways.users.insertEntry(this.id); } } diff --git a/src/lib/parsers/ArgResolver.js b/src/lib/parsers/ArgResolver.js index 5e3d67b4e5..97895468b0 100644 --- a/src/lib/parsers/ArgResolver.js +++ b/src/lib/parsers/ArgResolver.js @@ -14,7 +14,7 @@ class ArgResolver extends Resolver { * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command * @param {Function} custom The custom resolver - * @returns {Promise} + * @returns {*} */ async custom(arg, possible, msg, custom) { try { @@ -32,7 +32,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Piece} */ async piece(arg, possible, msg) { for (const store of this.client.pieceStores.values()) { @@ -48,7 +48,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Store} */ async store(arg, possible, msg) { const store = this.client.pieceStores.get(arg); @@ -64,7 +64,7 @@ class ArgResolver extends Resolver { * @param {KlasaMessage} msg The message that triggered the command * @returns {Promise} */ - async command(...args) { + command(...args) { return this.cmd(...args); } @@ -74,7 +74,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Command} */ async cmd(arg, possible, msg) { const command = this.client.commands.get(arg); @@ -88,7 +88,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Event} */ async event(arg, possible, msg) { const event = this.client.events.get(arg); @@ -102,7 +102,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Extendable} */ async extendable(arg, possible, msg) { const extendable = this.client.extendables.get(arg); @@ -116,7 +116,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Finalizer} */ async finalizer(arg, possible, msg) { const finalizer = this.client.finalizers.get(arg); @@ -130,7 +130,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Inhibitor} */ async inhibitor(arg, possible, msg) { const inhibitor = this.client.inhibitors.get(arg); @@ -144,7 +144,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Monitor} */ async monitor(arg, possible, msg) { const monitor = this.client.monitors.get(arg); @@ -158,7 +158,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Language} */ async language(arg, possible, msg) { const language = this.client.languages.get(arg); @@ -172,7 +172,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Provider} */ async provider(arg, possible, msg) { const provider = this.client.providers.get(arg); @@ -186,7 +186,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Task} */ async task(arg, possible, msg) { const task = this.client.tasks.get(arg); @@ -212,7 +212,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {KlasaMessage} */ async msg(arg, possible, msg) { const message = await super.msg(arg, msg.channel); @@ -238,7 +238,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {KlasaUser} */ async user(arg, possible, msg) { const user = await super.user(arg); @@ -252,7 +252,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {external:GuildMember} */ async member(arg, possible, msg) { const member = await super.member(arg, msg.guild); @@ -266,7 +266,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {external:Channel} */ async channel(arg, possible, msg) { const channel = await super.channel(arg); @@ -280,7 +280,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {external:Emoji} */ async emoji(arg, possible, msg) { const emoji = await super.emoji(arg); @@ -294,7 +294,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {KlasaGuild} */ async guild(arg, possible, msg) { const guild = await super.guild(arg); @@ -308,7 +308,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {external:Role} */ async role(arg, possible, msg) { const role = await super.role(arg, msg.guild); @@ -322,7 +322,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {string} */ async literal(arg, possible, msg) { arg = arg.toLowerCase(); @@ -348,7 +348,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {boolean} */ async bool(arg, possible, msg) { const boolean = await super.boolean(arg); @@ -374,7 +374,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {string} */ async str(arg, possible, msg) { const { min, max } = possible; @@ -401,7 +401,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {number} */ async int(arg, possible, msg) { const { min, max } = possible; @@ -442,7 +442,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {number} */ async float(arg, possible, msg) { const { min, max } = possible; @@ -460,7 +460,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {RegExpExecArray} */ async reg(arg, possible, msg) { const results = possible.regex.exec(arg); @@ -499,7 +499,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {string} */ async url(arg, possible, msg) { const hyperlink = await super.url(arg); @@ -513,7 +513,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Date} */ async date(arg, possible, msg) { const date = new Date(arg); @@ -527,7 +527,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Date} */ async duration(arg, possible, msg) { const date = new Duration(arg).fromNow; @@ -541,7 +541,7 @@ class ArgResolver extends Resolver { * @param {string} arg The argument to parse * @param {Possible} possible This current usage possible * @param {KlasaMessage} msg The message that triggered the command - * @returns {Promise} + * @returns {Date} */ async time(arg, possible, msg) { const date = await Promise.all([ diff --git a/src/lib/parsers/SettingResolver.js b/src/lib/parsers/SettingResolver.js index 8e6e0435c0..d88ff754f7 100644 --- a/src/lib/parsers/SettingResolver.js +++ b/src/lib/parsers/SettingResolver.js @@ -141,7 +141,7 @@ class SettingResolver extends Resolver { * @param {*} data The data to resolve * @param {KlasaGuild} guild The guild to resolve for * @param {string} name The name of the key being resolved - * @param {Object} minMax The minimum and maximum + * @param {Object} [minMax={}] The minimum and maximum * @param {?number} minMax.min The minimum value * @param {?number} minMax.max The maximum value * @returns {number} @@ -159,7 +159,7 @@ class SettingResolver extends Resolver { * @param {*} data The data to resolve * @param {KlasaGuild} guild The guild to resolve for * @param {string} name The name of the key being resolved - * @param {Object} minMax The minimum and maximum + * @param {Object} [minMax={}] The minimum and maximum * @param {?number} minMax.min The minimum value * @param {?number} minMax.max The maximum value * @returns {number} @@ -217,10 +217,10 @@ class SettingResolver extends Resolver { * Resolves anything, even objects. * @since 0.5.0 * @param {*} data Raw content to pass - * @returns {Promise<*>} + * @returns {*} */ - any(data) { - return Promise.resolve(data); + async any(data) { + return data; } /** diff --git a/src/lib/permissions/PermissionLevels.js b/src/lib/permissions/PermissionLevels.js index 1b7ba19523..a0cd64a8e3 100644 --- a/src/lib/permissions/PermissionLevels.js +++ b/src/lib/permissions/PermissionLevels.js @@ -14,7 +14,6 @@ class PermissionLevels extends Collection { * @typedef {Object} PermissionLevelsData * @property {boolean} broke Whether the loop broke execution of higher levels * @property {boolean} permission Whether the permission level check passed or not - * @memberof PermissionLevels */ /** diff --git a/src/lib/schedule/Schedule.js b/src/lib/schedule/Schedule.js index dc559ced7d..5fbff0151d 100644 --- a/src/lib/schedule/Schedule.js +++ b/src/lib/schedule/Schedule.js @@ -11,7 +11,6 @@ class Schedule { * @property {string} [id] The ID for the task. By default, it generates one in base36 * @property {string} [repeat] The {@link Cron} pattern * @property {*} [data] The data to pass to the Task piece when the ScheduledTask is ready for execution - * @memberof Schedule */ /** @@ -65,8 +64,8 @@ class Schedule { */ async init() { const { schema } = this.client.gateways.clientStorage; - if (!schema.hasKey('schedules')) { - await schema.addKey('schedules', { + if (!schema.has('schedules')) { + await schema.add('schedules', { type: 'any', default: [], min: null, @@ -136,7 +135,7 @@ class Schedule { * @param {string} taskName The name of the task * @param {(Date|number|string)} time The time or Cron pattern * @param {ScheduledTaskOptions} options The options for the ScheduleTask instance - * @returns {Promise} + * @returns {ScheduledTask} * @example * // Create a new reminder that ends in 2018-03-09T12:30:00.000Z (UTC) * Schedule.create('reminder', new Date(Date.UTC(2018, 2, 9, 12, 30)), { @@ -167,7 +166,7 @@ class Schedule { * Delete a Task by its ID * @since 0.5.0 * @param {string} id The ID to search for - * @returns {Promise} + * @returns {this} */ async delete(id) { const _task = this._tasks.find(entry => entry.id === id); @@ -242,6 +241,20 @@ class Schedule { else if (!this._interval) this._interval = this.client.setInterval(this.execute.bind(this), this.timeInterval); } + /** + * Returns a new Iterator object that contains the values for each element contained in the task queue. + * @name @@iterator + * @since 0.5.0 + * @method + * @instance + * @generator + * @returns {Iterator} + */ + + *[Symbol.iterator]() { + for (let i = 0; i < this.tasks.length; i++) yield this.tasks[i]; + } + } module.exports = Schedule; diff --git a/src/lib/schedule/ScheduledTask.js b/src/lib/schedule/ScheduledTask.js index df36e97b2a..fc46c58bc7 100644 --- a/src/lib/schedule/ScheduledTask.js +++ b/src/lib/schedule/ScheduledTask.js @@ -9,7 +9,6 @@ class ScheduledTask { * @typedef {Object} ScheduledTaskOptions * @property {string} [id] The ID for the task. By default, it generates one in base36 * @property {*} [data] The data to pass to the Task piece when the ScheduledTask is ready for execution - * @memberof ScheduledTask */ /** @@ -17,7 +16,6 @@ class ScheduledTask { * @property {string} [repeat] The {@link Cron} pattern * @property {Date} [time] The time the current task ends at * @property {*} [data] The data to pass to the Task piece when the ScheduledTask is ready for execution - * @memberof ScheduledTask */ /** @@ -27,7 +25,6 @@ class ScheduledTask { * @property {number} time The UNIX timestamp for when this task ends at * @property {string} [repeat] The {@link Cron} pattern * @property {*} [data] The data to pass to the Task piece when the ScheduledTask is ready for execution - * @memberof ScheduledTask */ /** diff --git a/src/lib/settings/Configuration.js b/src/lib/settings/Configuration.js index 9e10ea13c6..24e9276f8a 100644 --- a/src/lib/settings/Configuration.js +++ b/src/lib/settings/Configuration.js @@ -1,4 +1,6 @@ -const { isObject, makeObject, deepClone, tryParse } = require('../util/util'); +const { isObject, makeObject, deepClone, tryParse, getIdentifier, toTitleCase, arraysEqual, mergeObjects, getDeepTypeName } = require('../util/util'); +const SchemaFolder = require('./SchemaFolder'); +const SchemaPiece = require('./SchemaPiece'); /** * Creating your own Configuration instances if often discouraged and unneeded. SettingGateway handles them internally for you. @@ -8,9 +10,14 @@ class Configuration { /** * @typedef {Object} ConfigurationUpdateResult - * @property {*} value The parsed value - * @property {SchemaPiece} path The SchemaPiece that manages the updated value - * @memberof Configuration + * @property {Error[]} errors The errors caught from parsing + * @property {ConfigurationUpdateResultEntry[]} updated The updated keys + */ + + /** + * @typedef {Object} ConfigurationUpdateResultEntry + * @property {any[]} data A tuple containing the path of the updated key and the new value + * @property {SchemaPiece} piece The SchemaPiece instance that manages the updated key */ /** @@ -19,50 +26,6 @@ class Configuration { * @property {('add'|'remove'|'auto')} [action='auto'] Whether the update (when using arrays) should add or remove, * leave it as 'auto' to add or remove depending on the existence of the key in the array * @property {number} [arrayPosition] The position of the array to replace - * @memberof Configuration - */ - - /** - * @typedef {Object} ConfigurationUpdateObjectResult - * @property {ConfigurationUpdateObjectList} updated An object containing all keys and values updated - * @property {Error[]} errors All the errors caught by the parser - * @memberof Configuration - */ - - /** - * @typedef {Object} ConfigurationUpdateObjectList - * @property {string[]} keys An array of all updated keys - * @property {Array<*>} values An array of all updated values - * @memberof Configuration - */ - - /** - * @typedef {Object} ConfigurationPathResult - * @property {string} path The path of the updated key - * @property {string[]} route The path split by dots - * @memberof Configuration - * @private - */ - - /** - * @typedef {Object} ConfigurationUpdateManyList - * @property {Error[]} errors An array containing all the errors caught - * @property {Array>} promises An array of promises to resolve - * @property {string[]} keys An array of all keys pending for update - * @property {Array<*>} values An array of all values pending for update - * @memberof Configuration - * @private - */ - - /** - * @typedef {Object} ConfigurationParseResult - * @property {*} parsed The parsed value - * @property {(string|number|object)} parsedID The parsed ID value for DB storage - * @property {(null|Array<*>)} array The updated array. Can be null if the key doesn't hold an array - * @property {string} path The path of the updated key - * @property {string[]} route The path split by dots - * @memberof Configuration - * @private */ /** @@ -89,15 +52,6 @@ class Configuration { */ Object.defineProperty(this, 'gateway', { value: manager }); - /** - * The type of the Gateway. - * @since 0.5.0 - * @type {string} - * @name Configuration#type - * @readonly - */ - Object.defineProperty(this, 'type', { value: manager.type }); - /** * The ID that identifies this instance. * @since 0.5.0 @@ -125,11 +79,8 @@ class Configuration { */ Object.defineProperty(this, '_syncStatus', { value: null, writable: true }); - const { schema } = this.gateway; - for (let i = 0; i < schema.keyArray.length; i++) { - const key = schema.keyArray[i]; - this[key] = Configuration._merge(data[key], schema[key]); - } + Configuration._merge(data, this.gateway.schema); + for (const key of this.gateway.schema.keys()) this[key] = data[key]; } /** @@ -139,18 +90,8 @@ class Configuration { * @returns {*} */ get(key) { - if (!key.includes('.')) return this.gateway.schema.hasKey(key) ? this[key] : undefined; - - const path = key.split('.'); - let refSetting = this; // eslint-disable-line consistent-this - let refSchema = this.gateway.schema; - for (const currKey of path) { - if (refSchema.type !== 'Folder' || !refSchema.hasKey(currKey)) return undefined; - refSetting = refSetting[currKey]; - refSchema = refSchema[currKey]; - } - - return refSetting; + if (!key.includes('.')) return this.gateway.schema.has(key) ? this[key] : undefined; + return this._get(key.split('.'), true); } /** @@ -162,21 +103,10 @@ class Configuration { return new this.gateway.Configuration(this.gateway, this.gateway.Configuration._clone(this, this.gateway.schema)); } - /** - * Factory resets the current configuration. - * @since 0.5.0 - * @returns {Promise} - */ - async resetConfiguration() { - if (this._existsInDB) await this.gateway.provider.delete(this.gateway.type, this.id); - for (const key of this.gateway.schema.keyArray) this[key] = Configuration._merge(undefined, this.gateway.schema[key]); - return this; - } - /** * Sync the data from the database with the cache. * @since 0.5.0 - * @returns {Promise} + * @returns {this} */ async sync() { this._syncStatus = this.gateway.provider.get(this.gateway.type, this.id); @@ -192,30 +122,64 @@ class Configuration { /** * Delete this entry from the database and cache. * @since 0.5.0 - * @returns {Promise} + * @returns {this} */ async destroy() { if (this._existsInDB) { await this.gateway.provider.delete(this.gateway.type, this.id); if (this.client.listenerCount('configDeleteEntry')) this.client.emit('configDeleteEntry', this); } - this.gateway.cache.delete(this.gateway.type, this.id); + this.gateway.cache.delete(this.id); return this; } /** * Reset a value from an entry. * @since 0.5.0 - * @param {string} key The key to reset - * @param {boolean} [avoidUnconfigurable=false] Whether the Gateway should avoid configuring the selected key - * @returns {Promise} + * @param {(string|string[])} [keys] The key to reset + * @param {KlasaGuild} [guild] A KlasaGuild instance for multilanguage support + * @param {boolean} [avoidUnconfigurable] Whether the Gateway should avoid configuring the selected key + * @returns {ConfigurationUpdateResult} + * // Reset all keys for this instance + * Configuration#reset(); + * + * // Reset multiple keys for this instance + * Configuration#reset(['prefix', 'channels.modlog']); + * + * // Reset a key + * Configuration#reset('prefix'); */ - async reset(key, avoidUnconfigurable = false) { - const { parsedID, parsed, path } = await this._reset(key, avoidUnconfigurable); - await (this.gateway.sql ? - this.gateway.provider.update(this.gateway.type, this.id, key, parsedID) : - this.gateway.provider.update(this.gateway.type, this.id, makeObject(key, parsedID))); - return { value: parsed, path }; + async reset(keys, guild, avoidUnconfigurable = false) { + if (typeof guild === 'boolean') { + avoidUnconfigurable = guild; + guild = undefined; + } + + // If the entry does not exist in the DB, it'll never be able to reset a key + if (!this._existsInDB) return { errors: [], updated: [] }; + + if (typeof keys === 'string') keys = [keys]; + else if (typeof keys === 'undefined') keys = [...this.gateway.schema.values(true)].map(piece => piece.path); + if (Array.isArray(keys)) { + const result = { errors: [], updated: [] }; + const entries = new Array(keys.length); + for (let i = 0; i < keys.length; i++) entries[i] = [keys[i], null]; + for (const [key, value] of entries) { + const path = this.gateway.getPath(key, { piece: true, avoidUnconfigurable, errors: false }); + if (!path) { + result.errors.push(guild && guild.language ? + guild.language.get('COMMAND_CONF_GET_NOEXT', key) : + `The path ${key} does not exist in the current schema, or does not correspond to a piece.`); + continue; + } + const newValue = value === null ? deepClone(path.piece.default) : value; + const { updated } = this._setValueByPath(path.piece, newValue); + if (updated) result.updated.push({ data: [path.piece.path, newValue], piece: path.piece }); + } + if (result.updated.length) await this._save(result); + return result; + } + throw new TypeError(`Invalid value. Expected string or Array. Got: ${getDeepTypeName(keys)}`); } /** @@ -225,8 +189,7 @@ class Configuration { * @param {*} [value] The value to parse and save * @param {GuildResolvable} [guild] A guild resolvable * @param {ConfigurationUpdateOptions} [options={}] The options for the update - * @returns {Promise<(ConfigurationUpdateResult|ConfigurationUpdateObjectList)>} - * @throws {Promise} + * @returns {Promise} * @example * // Updating the value of a key * Configuration#update('roles.administrator', '339943234405007361', msg.guild); @@ -240,174 +203,231 @@ class Configuration { * // Updating it with a json object: * Configuration#update({ roles: { administrator: '339943234405007361' } }, msg.guild); * - * // Updating multiple keys (only possible with json object): + * // Updating multiple keys (with json object): * Configuration#update({ prefix: 'k!', language: 'es-ES' }, msg.guild); + * + * // Updating multiple keys (with arrays): + * Configuration#update(['prefix', 'language'], ['k!', 'es-ES']); */ - update(key, value, guild, options) { + async update(key, value, guild, options = {}) { if (typeof options === 'undefined' && isObject(guild)) { options = guild; guild = undefined; } if (guild) guild = this.gateway._resolveGuild(guild); + if (typeof key === 'string') { + key = [key]; + value = [value]; + } else if (isObject(key)) { + return this._updateMany(key, guild); + } - if (isObject(key)) return this._updateMany(key, value); - return this._updateSingle(key, value, guild, options); - } - - /** - * Update multiple keys given a JSON object. - * @since 0.5.0 - * @param {Object} object A JSON object to iterate and parse - * @param {GuildResolvable} [guild] A guild resolvable - * @returns {Promise} - * @throws {Promise} - * @private - */ - async _updateMany(object, guild) { - const list = { errors: [], promises: [], keys: [], values: [] }; + if (Array.isArray(key)) { + if (!Array.isArray(value) || key.length !== value.length) throw new Error(`Expected an array of ${key.length} entries. Got: ${value.length}.`); - // Handle entry creation if it does not exist. - if (!this._existsInDB) await this.gateway.createEntry(this.id); + // Create the result with all the promises to resolve + const result = { errors: [], updated: [] }; + const mps = new Array(key.length); + for (let i = 0; i < key.length; i++) mps[i] = this._parseSingle(key[i], value[i], guild, options, result); + await Promise.all(mps); - const oldClone = this.client.listenerCount('configUpdateEntry') ? this.clone() : null; - const updateObject = {}; - this._parseUpdateMany(this, object, this.gateway.schema, guild, list, updateObject); - await Promise.all(list.promises); - - if (oldClone !== null) this.client.emit('configUpdateEntry', oldClone, this, { type: 'MANY', keys: list.keys, values: list.values }); - if (this.gateway.sql) await this.gateway.provider.update(this.gateway.type, this.id, list.keys, list.values); - else await this.gateway.provider.update(this.gateway.type, this.id, updateObject); - if (list.errors.length) throw { updated: { keys: list.keys, values: list.values }, errors: list.errors }; - return { keys: list.keys, values: list.values }; + // If at least one key updated, save it to the database + if (result.updated.length) await this._save(result); + return result; + } + throw new TypeError(`Invalid value. Expected object, string or Array. Got: ${getDeepTypeName(key)}`); } /** - * Reset a value from an entry. + * Get a list. * @since 0.5.0 - * @param {string} key The key to reset - * @param {boolean} avoidUnconfigurable Whether the Gateway should avoid configuring the selected key - * @returns {Promise} - * @private + * @param {KlasaMessage} msg The Message instance + * @param {(SchemaFolder|string)} path The path to resolve + * @returns {string} */ - async _reset(key, avoidUnconfigurable) { - if (typeof key !== 'string') throw new TypeError(`The argument key must be a string. Received: ${typeof key}`); - const pathData = this.gateway.getPath(key, { avoidUnconfigurable, piece: true }); - return this._parseReset(key, pathData); + list(msg, path) { + const folder = path instanceof SchemaFolder ? path : this.gateway.getPath(path, { piece: false }).piece; + const array = []; + const folders = []; + const keys = {}; + let longest = 0; + for (const [key, value] of folder.entries()) { + if (value.type === 'Folder') { + if (value.configurableKeys.length) folders.push(`// ${key}`); + } else if (value.configurable) { + if (!(value.type in keys)) keys[value.type] = []; + if (key.length > longest) longest = key.length; + keys[value.type].push(key); + } + } + const keysTypes = Object.keys(keys); + if (folders.length === 0 && keysTypes.length === 0) return ''; + if (folders.length) array.push('= Folders =', ...folders.sort(), ''); + if (keysTypes.length) { + for (const keyType of keysTypes.sort()) { + keys[keyType].sort(); + array.push(`= ${toTitleCase(keyType)}s =`); + for (const key of keys[keyType]) array.push(`${key.padEnd(longest)} :: ${this.resolveString(msg, folder[key])}`); + array.push(''); + } + } + return array.join('\n'); } /** - * Parse the data for reset. + * Resolve a string. * @since 0.5.0 - * @param {string} key The key to edit - * @param {ConfigurationPathResult} options The options - * @returns {Promise} + * @param {KlasaMessage} msg The Message to use + * @param {(SchemaPiece|string)} path The path to resolve + * @returns {string} * @private */ - async _parseReset(key, { path, route }) { - const parsedID = deepClone(path.default); - await this._setValue(parsedID, path, route); - return { parsed: parsedID, parsedID, array: null, path, route }; + resolveString(msg, path) { + const piece = path instanceof SchemaPiece ? path : this.gateway.getPath(path, { piece: true }).piece; + const value = this.get(piece.path); + if (value === null) return 'Not set'; + if (piece.array && value.length === 0) return 'None'; + + let resolver; + switch (piece.type) { + case 'Folder': resolver = () => 'Folder'; + break; + case 'user': resolver = (val) => (this.client.users.get(val) || { username: (val && val.username) || val }).username; + break; + case 'categorychannel': + case 'textchannel': + case 'voicechannel': + case 'channel': resolver = (val) => (msg.guild.channels.get(val) || { name: (val && val.name) || val }).name; + break; + case 'role': resolver = (val) => (msg.guild.roles.get(val) || { name: (val && val.name) || val }).name; + break; + case 'guild': resolver = (val) => (val && val.name) || val; + break; + case 'boolean': resolver = (val) => val ? 'Enabled' : 'Disabled'; + break; + default: + resolver = (val) => val; + } + + if (piece.array) return `[ ${value.map(resolver).join(' | ')} ]`; + return resolver(value); } /** - * Update a single key + * Get a value from the cache. * @since 0.5.0 - * @param {string} key The key to edit - * @param {*} value The new value - * @param {GuildResolvable} guild The guild to take - * @param {ConfigurationPathResult} options The options - * @returns {Promise} + * @param {(string|string[])} route The route to get + * @param {boolean} piece Whether the get should resolve a piece or a folder + * @returns {*} * @private */ - async _parseUpdateOne(key, value, guild, { path, route }) { - if (path.array === true) throw 'This key is array type.'; + _get(route, piece = true) { + if (typeof route === 'string') route = route.split('.'); + let refCache = this, refSchema = this.gateway.schema; // eslint-disable-line consistent-this + for (const key of route) { + if (refSchema.type !== 'Folder' || !refSchema.has(key)) return undefined; + refCache = refCache[key]; + refSchema = refSchema[key]; + } - const parsed = await path.parse(value, guild); - const parsedID = Configuration.getIdentifier(parsed); - await this._setValue(parsedID, path, route); - return { parsed, parsedID, array: null, path, route }; + return piece && refSchema.type !== 'Folder' ? refCache : undefined; } /** - * Update an array + * Save the data to the database. * @since 0.5.0 - * @param {('add'|'remove'|'auto')} action Whether the value should be added or removed to the array - * @param {string} key The key to edit - * @param {*} value The new value - * @param {GuildResolvable} guild The guild to take - * @param {number} arrayPosition The array position to update - * @param {ConfigurationPathResult} options The options - * @returns {Promise} + * @param {ConfigurationUpdateResult} result The data to save * @private */ - async _parseUpdateArray(action, key, value, guild, arrayPosition, { path, route }) { - if (path.array === false) { - if (guild) throw guild.language.get('COMMAND_CONF_KEY_NOT_ARRAY'); - throw new Error('The key is not an array.'); + async _save({ updated }) { + if (!updated.length) return; + if (!this._existsInDB) { + await this.gateway.createEntry(this.id); + if (this.client.listenerCount('configCreateEntry')) this.client.emit('configCreateEntry', this); } - - const parsed = await path.parse(value, guild); - const parsedID = path.type !== 'any' ? Configuration.getIdentifier(parsed) : parsed; - - // Handle entry creation if it does not exist. - if (!this._existsInDB) await this.gateway.createEntry(this.id); const oldClone = this.client.listenerCount('configUpdateEntry') ? this.clone() : null; - let cache = this; // eslint-disable-line consistent-this - for (let i = 0; i < route.length - 1; i++) cache = cache[route[i]] || {}; - cache = cache[route[route.length - 1]] || []; - - if (typeof arrayPosition === 'number') { - if (arrayPosition >= cache.length) throw new Error(`The option arrayPosition should be a number between 0 and ${cache.length - 1}`); - cache[arrayPosition] = parsedID; + if (this.gateway.sql) { + const keys = new Array(updated.length), values = new Array(updated.length); + for (let i = 0; i < updated.length; i++) [keys[i], values[i]] = updated[i]; + await this.gateway.provider.update(this.gateway.type, this.id, keys, values); } else { - if (action === 'auto') action = cache.includes(parsedID) ? 'remove' : 'add'; - if (action === 'add') { - if (cache.includes(parsedID)) throw `The value ${parsedID} for the key ${path.path} already exists.`; - cache.push(parsedID); - } else { - const index = cache.indexOf(parsedID); - if (index === -1) throw `The value ${parsedID} for the key ${path.path} does not exist.`; - cache.splice(index, 1); - } + const updateObject = {}; + for (const entry of updated) mergeObjects(updateObject, makeObject(entry.data[0], entry.data[1])); + await this.gateway.provider.update(this.gateway.type, this.id, updateObject); } - if (oldClone !== null) this.client.emit('configUpdateEntry', oldClone, this, path.path); - return { parsed, parsedID, array: cache, path, route }; + if (oldClone !== null) this.client.emit('configUpdateEntry', oldClone, this, updated); } /** - * Update an array + * Update multiple keys given a JSON object. * @since 0.5.0 - * @param {string} key The key to edit - * @param {*} value The new value - * @param {GuildResolvable} guild The guild to take - * @param {Object} [options={}] The options - * @param {boolean} [options.avoidUnconfigurable=false] Whether the Gateway should avoid configuring the selected key - * @param {('add'|'remove'|'auto')} [options.action='auto'] Whether the value should be added or removed to the array - * @param {number} [options.arrayPosition=null] The array position to update - * @returns {Promise} + * @param {Object} object A JSON object to iterate and parse + * @param {GuildResolvable} [guild] A guild resolvable + * @returns {ConfigurationUpdateResult} * @private */ - async _updateSingle(key, value, guild, { avoidUnconfigurable = false, action = 'auto', arrayPosition = null } = {}) { - if (typeof key !== 'string') throw new TypeError(`The argument key must be a string. Received: ${typeof key}`); - if (typeof guild === 'boolean') { - avoidUnconfigurable = guild; - guild = undefined; - } + async _updateMany(object, guild) { + const result = { errors: [], updated: [], promises: [] }; - const pathData = this.gateway.getPath(key, { avoidUnconfigurable, piece: true }); - if (action === 'remove' && !pathData.path.array) return this._parseReset(key, pathData); - const { parsedID, array, parsed } = value === null || (action === 'remove' && !pathData.path.array) ? - this._parseReset(key, pathData) : - pathData.path.array === true ? - await this._parseUpdateArray(action, key, value, guild, arrayPosition, pathData) : - await this._parseUpdateOne(key, value, guild, pathData); + this._parseUpdateMany(this, object, this.gateway.schema, guild, result); + await Promise.all(result.promises); + delete result.promises; - if (this.gateway.sql) await this.gateway.provider.update(this.gateway.type, this.id, key, array || parsedID); - else await this.gateway.provider.update(this.gateway.type, this.id, makeObject(key, array || parsedID)); + // If at least one key updated, save it to the database + if (result.updated.length) await this._save(result); + return result; + } - return { value: parsed, path: pathData.path }; + /** + * Parse a single value + * @since 0.5.0 + * @param {string} key The key to update + * @param {*} value The new value for the key + * @param {?KlasaGuild} guild The Guild instance for key resolving + * @param {ConfigurationUpdateOptions} options The options for parsing this value + * @param {ConfigurationUpdateResult} list The list to update + * @private + */ + async _parseSingle(key, value, guild, { avoidUnconfigurable = false, action = 'auto', arrayPosition = null }, list) { + const path = this.gateway.getPath(key, { piece: true, avoidUnconfigurable, errors: false }); + if (!path) { + list.errors.push(guild && guild.language ? + guild.language.get('COMMAND_CONF_GET_NOEXT', key) : + `The path ${key} does not exist in the current schema, or does not correspond to a piece.`); + return; + } + const { piece, route } = path; + let parsed, parsedID; + if (value === null) { + parsed = parsedID = deepClone(piece.default); + } else { + parsed = await piece.parse(value, guild); + parsedID = piece.type === 'any' ? parsed : getIdentifier(parsed); + } + + if (piece.array) { + const array = this._get(route, true); + if (typeof arrayPosition === 'number') { + if (arrayPosition >= array.length) throw new Error(`The option arrayPosition should be a number between 0 and ${array.length - 1}`); + array[arrayPosition] = parsedID; + } else { + if (action === 'auto') action = array.includes(parsedID) ? 'remove' : 'add'; + if (action === 'add') { + if (array.includes(parsedID)) throw `The value ${parsedID} for the key ${piece.path} already exists.`; + array.push(parsedID); + } else { + const index = array.indexOf(parsedID); + if (index === -1) throw `The value ${parsedID} for the key ${piece.path} does not exist.`; + array.splice(index, 1); + } + } + list.updated.push({ data: [piece.path, parsedID], piece: piece }); + } else { + const { updated } = this._setValueByPath(piece, parsedID); + if (updated) list.updated.push({ data: [piece.path, parsedID], piece: piece }); + } } /** @@ -417,69 +437,58 @@ class Configuration { * @param {Object} object The key to edit * @param {SchemaFolder} schema The new value * @param {GuildResolvable} guild The guild to take - * @param {ConfigurationUpdateManyList} list The options - * @param {*} updateObject The object to update + * @param {ConfigurationUpdateResult} result The options * @private */ - _parseUpdateMany(cache, object, schema, guild, list, updateObject) { + _parseUpdateMany(cache, object, schema, guild, result) { for (const key of Object.keys(object)) { - if (!schema.hasKey(key)) continue; + if (!schema.has(key)) continue; if (schema[key].type === 'Folder') { - if (!(key in updateObject)) updateObject = updateObject[key] = {}; - this._parseUpdateMany(cache[key], object[key], schema[key], guild, list, updateObject); - } else if (schema[key].array && !Array.isArray(object[key])) { - list.errors.push([schema[key].path, new Error(`${schema[key].path} expects an array as value.`)]); - } else if (!schema[key].array && schema[key].array !== 'any' && Array.isArray(object[key])) { - list.errors.push([schema[key].path, new Error(`${schema[key].path} does not expect an array as value.`)]); + // Check if it's a folder, and recursively iterate over it + this._parseUpdateMany(cache[key], object[key], schema[key], guild, result); } else if (object[key] === null) { - list.promises.push( - this.reset(key) - .then(({ value, path }) => { - updateObject[key] = cache[key] = value; - list.keys.push(path); - list.values.push(value); - }) - .catch(error => list.errors.push([schema.path, error])) - ); + // If the value is null, reset it + const defaultValue = deepClone(schema[key].default); + if (this._setValueByPath(schema[key], defaultValue).updated) { + result.updated.push({ data: [schema[key].path, defaultValue], piece: schema[key] }); + } + // Throw an error if it's not array (nor type any) but an array was given + } else if (schema[key].array !== Array.isArray(object[key])) { + result.errors.push(new TypeError(schema[key].array ? + `${schema[key].path} expects an array as value.` : + `${schema[key].path} does not expect an array as value.`)); } else { - const promise = schema[key].array && schema[key].type !== 'any' ? - Promise.all(object[key].map(entry => schema[key].parse(entry, guild) - .then(Configuration.getIdentifier) - .catch(error => list.errors.push([schema[key].path, error])))) : - schema[key].parse(object[key], guild); - - list.promises.push(promise - .then(parsed => { - const parsedID = schema[key].array ? - parsed.filter(entry => typeof entry !== 'undefined') : - Configuration.getIdentifier(parsed); - updateObject[key] = cache[key] = parsedID; - list.keys.push(schema[key].path); - list.values.push(parsedID); - }) - .catch(error => list.errors.push([schema.path, error]))); + const getID = schema[key].type !== 'any' ? getIdentifier : entry => entry; + result.promises.push((schema[key].array ? + Promise.all(object[key].map(entry => schema[key].parse(entry, guild).then(getID))) : + schema[key].parse(object[key], guild)).then(parsed => { + if (this._setValueByPath(schema[key], parsed).updated) { + result.updated.push({ data: [schema[key].path, parsed], piece: schema[key] }); + } + }).catch(error => { result.errors.push(error); })); } } } /** - * Set a value at a certain path + * Set a value by its path * @since 0.5.0 - * @param {string} parsedID The parsed ID or result - * @param {SchemaPiece} path The SchemaPiece which handles the key to modify - * @param {string[]} route The route of the key to modify + * @param {SchemaPiece} piece The piece that manages the key + * @param {*} parsedID The parsed ID value + * @returns {{ updated: boolean, old: any }} * @private */ - async _setValue(parsedID, path, route) { - // Handle entry creation if it does not exist. - if (!this._existsInDB) await this.gateway.createEntry(this.id); - const oldClone = this.client.listenerCount('configUpdateEntry') ? this.clone() : null; - + _setValueByPath(piece, parsedID) { + const path = piece.path.split('.'); + const lastKey = path.pop(); let cache = this; // eslint-disable-line consistent-this - for (let i = 0; i < route.length - 1; i++) cache = cache[route[i]] || {}; - cache[route[route.length - 1]] = parsedID; - - if (oldClone !== null) this.client.emit('configUpdateEntry', oldClone, this, path.path); + for (const key of path) cache = cache[key] || {}; + const old = cache[lastKey]; + if (piece.array ? !arraysEqual(old, parsedID, true) : old !== parsedID) { + cache[lastKey] = parsedID; + return { updated: true, old }; + } + return { updated: false, old }; } /** @@ -494,8 +503,9 @@ class Configuration { for (let i = 0; i < schema.keyArray.length; i++) { const key = schema.keyArray[i]; if (typeof data[key] === 'undefined') continue; - if (schema[key].type === 'Folder') Configuration._patch(this[key], data[key], schema[key]); - else this[key] = data[key]; + this[key] = schema[key].type === 'Folder' ? + Configuration._patch(this[key], data[key], schema[key]) : + data[key]; } } @@ -526,22 +536,18 @@ class Configuration { * @private */ static _merge(data, schema) { - if (schema.type === 'Folder') { - if (typeof data === 'undefined') data = {}; - for (let i = 0; i < schema.keyArray.length; i++) { - const key = schema.keyArray[i]; - data[key] = Configuration._merge(data[key], schema[key]); + for (const [key, piece] of schema) { + if (piece.type === 'Folder') { + if (!data[key]) data[key] = {}; + data[key] = Configuration._merge(data[key], piece); + } else if (typeof data[key] === 'undefined' || data[key] === null) { + data[key] = deepClone(piece.default); + } else if (piece.array) { + if (typeof data[key] === 'string') data[key] = tryParse(data[key]); + if (Array.isArray(data[key])) continue; + piece.client.emit('wtf', + new TypeError(`${piece.path} | Expected an array, null, or undefined. Got: ${Object.prototype.toString.call(data[key])}`)); } - } else if (typeof data === 'undefined') { - // It's a SchemaPiece instance, so it has a property of 'key'. - data = deepClone(schema.default); - } else if (schema.array) { - if (Array.isArray(data)) return data; - // Some SQL databases are unable to store Arrays... - if (data === null) return deepClone(schema.default); - if (typeof data === 'string') return tryParse(data); - this.client.emit('wtf', - new TypeError(`${this} - ${schema.path} | Expected an array, null, or undefined. Got: ${Object.prototype.toString.call(data)}`)); } return data; @@ -558,10 +564,8 @@ class Configuration { static _clone(data, schema) { const clone = {}; - for (let i = 0; i < schema.keyArray.length; i++) { - const key = schema.keyArray[i]; - if (schema[key].type === 'Folder') clone[key] = Configuration._clone(data[key], schema[key]); - else clone[key] = deepClone(data[key]); + for (const [key, piece] of schema) { + clone[key] = piece.type === 'Folder' ? Configuration._clone(data[key], piece) : deepClone(data[key]); } return clone; @@ -573,30 +577,18 @@ class Configuration { * @param {Object} inst The reference of the Configuration instance * @param {Object} data The original object * @param {SchemaFolder} schema A SchemaFolder instance + * @returns {Object} * @private */ static _patch(inst, data, schema) { - for (let i = 0; i < schema.keyArray.length; i++) { - const key = schema.keyArray[i]; + for (const key of schema.keys()) { if (typeof data[key] === 'undefined') continue; inst[key] = schema[key].type === 'Folder' ? Configuration._patch(inst[key], data[key], schema[key]) : data[key]; } - } - /** - * Get the identifier of a value. - * @since 0.5.0 - * @param {*} value The value to get the identifier from - * @returns {*} - * @private - */ - static getIdentifier(value) { - if (typeof value !== 'object' || value === null) return value; - if (value.id) return value.id; - if (value.name) return value.name; - return value; + return inst; } } diff --git a/src/lib/settings/Gateway.js b/src/lib/settings/Gateway.js index 7688c53baa..ec433a2829 100644 --- a/src/lib/settings/Gateway.js +++ b/src/lib/settings/Gateway.js @@ -2,7 +2,8 @@ const GatewayStorage = require('./GatewayStorage'); const Configuration = require('./Configuration'); const SchemaPiece = require('./SchemaPiece'); const SchemaFolder = require('./SchemaFolder'); -const discord = require('discord.js'); +const { Collection, Guild, GuildChannel, Message, Role, GuildMember } = require('discord.js'); +const { getIdentifier } = require('../util/util'); /** * You should never create a Gateway instance by yourself. @@ -12,41 +13,37 @@ const discord = require('discord.js'); */ class Gateway extends GatewayStorage { - /** - * @typedef {Object} GatewayOptions - * @property {Provider} [provider] The provider to use - * @property {boolean} [nice=false] Whether the JSON provider should use sequential or burst mode - * @memberof Gateway - */ - /** * @typedef {Object} GatewayGetPathOptions * @property {boolean} [avoidUnconfigurable=false] Whether the getPath should avoid unconfigurable keys * @property {boolean} [piece=true] Whether the getPath should return pieces or folders - * @memberof Gateway */ /** * @typedef {Object} GatewayGetPathResult - * @property {SchemaPiece} path The resolved path + * @property {SchemaPiece} piece The piece resolved from the path * @property {string[]} route The resolved path split by dots - * @memberof Gateway */ /** * @typedef {(KlasaGuild|KlasaMessage|external:TextChannel|external:VoiceChannel|external:CategoryChannel|external:GuildMember|external:Role)} GuildResolvable - * @memberof Gateway + */ + + /** + * @typedef {Object} GatewayJSON + * @property {string} type The name of this gateway + * @property {GatewayDriverAddOptions} options The options for this gateway + * @property {Object} schema The current schema */ /** * @since 0.0.1 * @param {GatewayDriver} store The GatewayDriver instance which initiated this instance * @param {string} type The name of this Gateway - * @param {Function} validateFunction The function that validates the entries' values * @param {Object} schema The initial schema for this instance - * @param {GatewayOptions} options The options for this schema + * @param {GatewayDriverAddOptions} options The options for this schema */ - constructor(store, type, validateFunction, schema, options) { + constructor(store, type, schema, options) { super(store.client, type, options.provider); /** @@ -57,21 +54,21 @@ class Gateway extends GatewayStorage { /** * @since 0.5.0 - * @type {GatewayOptions} + * @type {GatewayDriverAddOptions} */ this.options = options; /** * @since 0.3.0 - * @type {Function} + * @type {Object} */ - this.validate = validateFunction; + this.defaultSchema = schema; /** - * @since 0.3.0 - * @type {Object} + * @since 0.0.1 + * @type {external:Collection} */ - this.defaultSchema = schema; + this.cache = new Collection(); } /** @@ -85,16 +82,6 @@ class Gateway extends GatewayStorage { return Configuration; } - /** - * Get the cache-provider that manages the cache data. - * @since 0.0.1 - * @type {Provider} - * @readonly - */ - get cache() { - return this.client.providers.get('collection'); - } - /** * @since 0.0.1 * @type {SettingResolver} @@ -115,38 +102,39 @@ class Gateway extends GatewayStorage { getEntry(input, create = false) { if (input === 'default') return this.defaults; if (create) { - const entry = this.cache.get(this.type, input); + const entry = this.cache.get(input); if (!entry) { const configs = new this.Configuration(this, { id: input }); - this.cache.set(this.type, input, configs); + this.cache.set(input, configs); // Silently create a new entry. The new data does not matter as Configuration default all the keys. this.provider.create(this.type, input) .then(() => { configs.existsInDB = true; if (this.client.listenerCount('configCreateEntry')) this.client.emit('configCreateEntry', configs); }) - .catch(error => this.client.emit('log', error, 'error')); + .catch(error => this.client.emit('error', error)); return configs; } return entry; } - return this.cache.get(this.type, input) || this.defaults; + return this.cache.get(input) || this.defaults; } /** * Create a new entry into the database with an optional content (defaults to this Gateway's defaults). * @since 0.5.0 * @param {string} input The name of the key to create - * @returns {Promise} + * @returns {Configuration} */ async createEntry(input) { - const target = await this.validate(input).then(output => output && output.id ? output.id : output); - const cache = this.cache.get(this.type, target); + const target = getIdentifier(input); + if (!target) throw new TypeError('The selected target could not be resolved to a string.'); + const cache = this.cache.get(target); if (cache && cache.existsInDB) return cache; await this.provider.create(this.type, target); const configs = cache || new this.Configuration(this, { id: target }); configs.existsInDB = true; - if (!cache) this.cache.set(this.type, target, configs); + if (!cache) this.cache.set(target, configs); if (this.client.listenerCount('configCreateEntry')) this.client.emit('configCreateEntry', configs); return configs; } @@ -160,8 +148,8 @@ class Gateway extends GatewayStorage { */ insertEntry(id, data = {}) { const configs = new this.Configuration(this, { ...data, id }); - this.cache.set(this.type, id, configs); - if (this.ready) configs.sync().catch(err => this.client.emit('error', err)); + this.cache.set(id, configs); + if (this.ready && this.schema.keyArray.length) configs.sync().catch(err => this.client.emit('error', err)); return configs; } @@ -169,10 +157,10 @@ class Gateway extends GatewayStorage { * Delete an entry from the database and cache. * @since 0.5.0 * @param {string} input The name of the key to fetch and delete - * @returns {Promise} + * @returns {boolean} */ async deleteEntry(input) { - const configs = this.cache.get(this.type, input); + const configs = this.cache.get(input); if (!configs) return false; await configs.destroy(); @@ -184,30 +172,33 @@ class Gateway extends GatewayStorage { * @since 0.0.1 * @param {(Object|string)} [input] An object containing a id property, like discord.js objects, or a string * @param {boolean} [download] Whether the sync should download data from the database - * @returns {Promise<*>} + * @returns {?Configuration} */ async sync(input, download) { if (typeof input === 'undefined') { - if (!download) return Promise.all(this.cache.getValues(this.type).map(entry => entry.sync())); + if (!download) return Promise.all(this.cache.map(entry => entry.sync())); const entries = await this.provider.getAll(this.type); for (const entry of entries) { - const cache = this.cache.get(this.type, entry); + const cache = this.cache.get(entry); if (cache) { if (!cache.existsInDB) cache.existsInDB = true; cache._patch(entry); } else { const newEntry = new this.Configuration(this, entry); newEntry.existsInDB = true; - this.cache.set(this.type, entry.id, newEntry); + this.cache.set(entry.id, newEntry); } } + return null; } - const target = await this.validate(input).then(output => output && output.id ? output.id : output); - const cache = this.cache.get(this.type, target); + const target = getIdentifier(input); + if (!target) throw new TypeError('The selected target could not be resolved to a string.'); + + const cache = this.cache.get(target); if (cache) return cache.sync(); const configs = new this.Configuration(this, { id: target }); - this.cache.set(this.type, target, configs); + this.cache.set(target, configs); return configs.sync(); } @@ -216,33 +207,41 @@ class Gateway extends GatewayStorage { * @since 0.5.0 * @param {string} [key=null] A string to resolve * @param {GatewayGetPathOptions} [options={}] Whether the Gateway should avoid configuring the selected key - * @returns {GatewayPathResult} + * @returns {?GatewayGetPathResult} */ - getPath(key = '', { avoidUnconfigurable = false, piece = true } = {}) { - if (key === '') return { path: this.schema, route: [] }; - if (typeof key !== 'string') throw new TypeError('The value for the argument \'key\' must be a string.'); + getPath(key = '', { avoidUnconfigurable = false, piece = true, errors = true } = {}) { + if (key === '') return { piece: this.schema, route: [] }; const route = key.split('.'); - let path = this.schema; + let { schema } = this; for (let i = 0; i < route.length; i++) { const currKey = route[i]; - if (typeof path[currKey] === 'undefined' || !path.hasKey(currKey)) throw `The key ${route.slice(0, i + 1).join('.')} does not exist in the current schema.`; + if (typeof schema[currKey] === 'undefined' || !schema.has(currKey)) { + if (errors) throw `The key ${route.slice(0, i + 1).join('.')} does not exist in the current schema.`; + return null; + } - if (path[currKey].type === 'Folder') { - path = path[currKey]; + if (schema[currKey].type === 'Folder') { + schema = schema[currKey]; } else if (piece) { - if (avoidUnconfigurable && !path[currKey].configurable) throw `The key ${path[currKey].path} is not configurable in the current schema.`; - return { path: path[currKey], route: path[currKey].path.split('.') }; + if (avoidUnconfigurable && !schema[currKey].configurable) { + if (errors) throw `The key ${schema[currKey].path} is not configurable in the current schema.`; + return null; + } + return { piece: schema[currKey], route: schema[currKey].path.split('.') }; } } - if (piece && path.type === 'Folder') { - const keys = path.configurableKeys; - if (keys.length === 0) throw `This group is not configurable.`; - throw `Please, choose one of the following keys: '${keys.join('\', \'')}'`; + if (piece && schema.type === 'Folder') { + const keys = schema.configurableKeys; + if (keys.length === 0) { + if (errors) throw `This group is not configurable.`; + return null; + } + if (errors) throw `Please, choose one of the following keys: '${keys.join('\', \'')}'`; } - return { path, route: path.path.split('.') }; + return { piece: schema, route: schema.path.split('.') }; } /** @@ -256,7 +255,6 @@ class Gateway extends GatewayStorage { await this.initSchema(); await this.initTable(); - if (!this.cache.hasTable(this.type)) this.cache.createTable(this.type); if (download) await this.sync(); this.ready = true; @@ -265,16 +263,16 @@ class Gateway extends GatewayStorage { /** * Readies up all Configuration instances in this gateway * @since 0.5.0 - * @returns {Promise>>} + * @returns {Array>} * @private */ async _ready() { - if (typeof this.client[this.type] === 'undefined') return null; + if (!this.schema.keyArray.length || typeof this.client[this.type] === 'undefined') return null; const promises = []; const keys = await this.provider.getKeys(this.type); for (let i = 0; i < keys.length; i++) { const structure = this.client[this.type].get(keys[i]); - if (structure) promises.push(structure.configs.sync().then(() => this.cache.set(this.type, keys[i], structure.configs))); + if (structure) promises.push(structure.configs.sync().then(() => this.cache.set(keys[i], structure.configs))); } const results = await Promise.all(promises); if (!this.ready) this.ready = true; @@ -291,8 +289,11 @@ class Gateway extends GatewayStorage { */ _resolveGuild(guild) { if (typeof guild === 'object') { - if (guild instanceof discord.Guild) return guild; - if (guild instanceof discord.GuildChannel || guild instanceof discord.Message || guild instanceof discord.Role || guild instanceof discord.GuildMember) return guild.guild; + if (guild instanceof Guild) return guild; + if (guild instanceof GuildChannel || + guild instanceof Message || + guild instanceof Role || + guild instanceof GuildMember) return guild.guild; } if (typeof guild === 'string' && /^\d{17,19}$/.test(guild)) return this.client.guilds.get(guild); return null; @@ -326,6 +327,19 @@ class Gateway extends GatewayStorage { if (force) await route.force(action, key, piece); } + /** + * Get a JSON object containing the schema and options. + * @since 0.5.0 + * @returns {GatewayJSON} + */ + toJSON() { + return { + type: this.type, + options: this.options, + schema: this.schema.toJSON() + }; + } + /** * Stringify a value or the instance itself. * @since 0.5.0 diff --git a/src/lib/settings/GatewayDriver.js b/src/lib/settings/GatewayDriver.js index 8667a3263d..c26b5d7894 100644 --- a/src/lib/settings/GatewayDriver.js +++ b/src/lib/settings/GatewayDriver.js @@ -11,7 +11,6 @@ class GatewayDriver { * @typedef {Object} GatewayDriverAddOptions * @property {string} [provider] The name of the provider to use * @property {boolean} [nice=false] Whether the JSON provider should use sequential or burst mode - * @memberof GatewayDriver */ /** @@ -20,7 +19,6 @@ class GatewayDriver { * @property {SchemaPieceJSON} language The per-guild's configurable language key * @property {SchemaPieceJSON} disableNaturalPrefix The per-guild's configurable disableNaturalPrefix key * @property {SchemaPieceJSON} disabledCommands The per-guild's configurable disabledCommands key - * @memberof GatewayDriver * @private */ @@ -29,7 +27,6 @@ class GatewayDriver { * @property {SchemaPieceJSON} userBlacklist The client's configurable user blacklist key * @property {SchemaPieceJSON} guildBlacklist The client's configurable guild blacklist key * @property {SchemaPieceJSON} schedules The schedules where {@link ScheduledTask}s are stored at - * @memberof GatewayDriver * @private */ @@ -47,6 +44,16 @@ class GatewayDriver { */ Object.defineProperty(this, 'client', { value: client }); + /** + * The register creation queue. + * @since 0.5.0 + * @name GatewayDriver#_queue + * @type {Map} + * @readonly + * @private + */ + Object.defineProperty(this, '_queue', { value: new Map() }); + /** * The resolver instance this Gateway uses to parse the data. * @type {SettingResolver} @@ -55,15 +62,15 @@ class GatewayDriver { /** * All the types accepted for the Gateway. - * @type {string[]} + * @type {Set} */ - this.types = Object.getOwnPropertyNames(SettingResolver.prototype).slice(1); + this.types = new Set(Object.getOwnPropertyNames(SettingResolver.prototype).slice(1)); /** - * All the caches added - * @type {string[]} + * All the gateways added + * @type {Set} */ - this.caches = []; + this.keys = new Set(); /** * If the driver is ready @@ -176,49 +183,55 @@ class GatewayDriver { } /** - * Add a new instance of SettingGateway, with its own validateFunction and schema. + * Registers a new Gateway. + * @since 0.5.0 + * @param {string} name The name for the new gateway + * @param {Object} [schema={}] The schema for use in this gateway + * @param {GatewayDriverAddOptions} [options={}] The options for the new gateway + * @chainable + * @returns {this} + */ + register(name, schema = {}, options = {}) { + if (!this.ready) { + if (this._queue.has(name)) throw new Error(`There is already a Gateway with the name '${name}'.`); + this._queue.set(name, () => { + this._register(name, schema, options); + this._queue.delete(name); + }); + } else { + this._register(name, schema, options); + } + + return this; + } + + /** + * Registers a new Gateway and inits it. * @since 0.3.0 * @param {string} name The name for the new instance - * @param {Function} validateFunction The function that validates the input * @param {Object} [schema={}] The schema * @param {GatewayDriverAddOptions} [options={}] A provider to use. If not specified it'll use the one in the client * @param {boolean} [download=true] Whether this Gateway should download the data from the database at init * @returns {Gateway} * @example - * // Add a new SettingGateway instance, called 'users', which input takes users, and stores a quote which is a string between 2 and 140 characters. - * const validate = async function(resolver, user) { - * const result = await resolver.user(user); - * if (!result) throw 'The parameter expects either a User ID or a User Object.'; - * return result; - * }; - * const schema = { - * quote: { - * type: 'String', - * default: null, - * array: false, - * min: 2, - * max: 140, - * }, - * }; - * GatewayDriver.add('users', validate, schema); + * // Add a new SettingGateway instance, called 'channels', that stores + * // disabled commands and a command throttle for custom ratelimits. + * this.client.gateways.add('channels', { + * disabledCommands: { + * type: 'Command', + * default: [] + * }, + * commandThrottle: { + * type: 'Integer', + * default: 5, + * min: 0, + * max: 60 + * } + * }); */ - async add(name, validateFunction, schema = {}, options = {}, download = true) { - if (typeof name !== 'string') throw 'You must pass a name for your new gateway and it must be a string.'; - - if (this[name] !== undefined && this[name] !== null) throw 'There is already a Gateway with that name.'; - if (typeof validateFunction !== 'function') throw 'You must pass a validate function.'; - validateFunction = validateFunction.bind(this); - if (!this.client.methods.util.isObject(schema)) throw 'Schema must be a valid object or left undefined for an empty object.'; - - options.provider = this._checkProvider(options.provider || this.client.options.providers.default); - const provider = this.client.providers.get(options.provider); - if (provider.cache) throw `The provider ${provider.name} is designed for caching, not persistent data. Please try again with another.`; - options.cache = this._checkProvider('collection'); - - const gateway = new Gateway(this, name, validateFunction, schema, options); + async add(name, schema = {}, options = {}, download = true) { + const gateway = this._register(name, schema, options); await gateway.init(download); - this.caches.push(name); - this[name] = gateway; return gateway; } @@ -226,20 +239,48 @@ class GatewayDriver { /** * Readies up all Gateways and Configuration instances * @since 0.5.0 - * @returns {Promise>>>} + * @returns {Array>>} * @private */ - _ready() { - if (this.ready) throw 'Configuration has already run the ready method.'; + async _ready() { + if (this.ready) throw new Error('Configuration has already run the ready method.'); this.ready = true; const promises = []; - for (const cache of this.caches) { - this[cache].ready = true; - promises.push(this[cache]._ready()); + for (const register of this._queue.values()) register(); + for (const key of this.keys) { + // If the gateway did not init yet, init it now + if (!this[key].ready) await this[key].init(); + promises.push(this[key]._ready()); } return Promise.all(promises); } + /** + * Registers a new Gateway + * @since 0.5.0 + * @param {string} name The name for the new gateway + * @param {Object} schema The schema for use in this gateway + * @param {GatewayDriverAddOptions} options The options for the new gateway + * @returns {Gateway} + * @private + */ + _register(name, schema, options) { + if (typeof name !== 'string') throw new Error('You must pass a name for your new gateway and it must be a string.'); + + if (this[name] !== undefined && this[name] !== null) throw new Error(`There is already a Gateway with the name '${name}'.`); + if (!this.client.methods.util.isObject(schema)) throw new Error('Schema must be a valid object or left undefined for an empty object.'); + + options.provider = this._checkProvider(options.provider || this.client.options.providers.default); + const provider = this.client.providers.get(options.provider); + if (provider.cache) throw new Error(`The provider ${provider.name} is designed for caching, not persistent data. Please try again with another.`); + + const gateway = new Gateway(this, name, schema, options); + this.keys.add(name); + this[name] = gateway; + + return gateway; + } + /** * Check if a provider exists. * @since 0.5.0 @@ -249,7 +290,32 @@ class GatewayDriver { */ _checkProvider(engine) { if (this.client.providers.has(engine)) return engine; - throw `This provider (${engine}) does not exist in your system.`; + throw new Error(`This provider (${engine}) does not exist in your system.`); + } + + /** + * The GatewayDriver with all gateways, types and keys as JSON. + * @since 0.5.0 + * @returns {Object} + */ + toJSON() { + const object = { + types: [...this.types], + keys: [...this.keys], + ready: this.ready + }; + for (const key of this.keys) object[key] = this[key].toJSON(); + + return object; + } + + /** + * The stringified GatewayDriver with all the managed gateways. + * @since 0.5.0 + * @returns {string} + */ + toString() { + return `GatewayDriver(${[...this.keys].join(', ')})`; } } diff --git a/src/lib/settings/GatewayStorage.js b/src/lib/settings/GatewayStorage.js index edda248dbc..440755bf1f 100644 --- a/src/lib/settings/GatewayStorage.js +++ b/src/lib/settings/GatewayStorage.js @@ -138,7 +138,7 @@ class GatewayStorage { /** * Inits the schema, creating a file if it does not exist, and returning the current schema or the default. * @since 0.5.0 - * @returns {Promise} + * @returns {SchemaFolder} * @private */ async initSchema() { @@ -179,20 +179,6 @@ class GatewayStorage { return object; } - /** - * Make an error that can or not have a valid Guild. - * @since 0.5.0 - * @param {KlasaGuild} guild The guild to get the language from - * @param {(string|number)} code The code of the error - * @param {(string|Error)} error The error - * @returns {string} - * @private - */ - static throwError(guild, code, error) { - if (guild && guild.language && typeof guild.language.get === 'function') return guild.language.get(code); - return `ERROR: [${code}]: ${error}`; - } - /** * Parse SQL values. * @since 0.5.0 diff --git a/src/lib/settings/SchemaFolder.js b/src/lib/settings/SchemaFolder.js index 7712e84d30..6e2cbeaa1c 100644 --- a/src/lib/settings/SchemaFolder.js +++ b/src/lib/settings/SchemaFolder.js @@ -1,10 +1,10 @@ const SchemaPiece = require('./SchemaPiece'); const Schema = require('./Schema'); -const { toTitleCase, deepClone } = require('../util/util'); +const { deepClone, isObject } = require('../util/util'); const fs = require('fs-nextra'); /** - * You should never create an instance of this class. Use {@link SchemaFolder#addFolder} instead. + * You should never create an instance of this class. Use {@link SchemaFolder#add} instead. * The schema class that stores (nested) folders and keys for SettingGateway usage. This class also implements multiple helpers. */ class SchemaFolder extends Schema { @@ -18,7 +18,6 @@ class SchemaFolder extends Schema { * @property {boolean} [array] Whether the key should be stored as Array or not * @property {string} [sql] The datatype of the key * @property {boolean} [configurable] Whether the key should be configurable by the config command or not - * @memberof SchemaFolder */ /** @@ -41,22 +40,6 @@ class SchemaFolder extends Schema { */ Object.defineProperty(this, 'type', { value: 'Folder' }); - /** - * The default values for this schema instance and children. - * @since 0.5.0 - * @type {Object} - * @name SchemaFolder#defaults - */ - Object.defineProperty(this, 'defaults', { value: {}, writable: true }); - - /** - * A Set containing all keys' names which value is either a SchemaFolder or a SchemaPiece instance. - * @since 0.5.0 - * @type {Set} - * @name SchemaFolder#keys - */ - Object.defineProperty(this, 'keys', { value: new Set(), writable: true }); - /** * A pre-processed array with all keys' names. * @since 0.5.0 @@ -76,64 +59,103 @@ class SchemaFolder extends Schema { */ get configurableKeys() { if (this.keyArray.length === 0) return []; - return this.keyArray.filter(key => this[key].type === 'Folder' || this[key].configurable); + return this.keyArray.filter(key => this[key].type === 'Folder' ? this[key].configurableKeys.length : this[key].configurable); + } + + /** + * The default values for this schema instance and children. + * @since 0.5.0 + * @type {Object} + * @readonly + */ + get defaults() { + const defaults = {}; + for (const [key, value] of this) { + defaults[key] = value.type === 'Folder' ? value.defaults : value.default; + } + return defaults; } /** * Create a new nested folder. * @since 0.5.0 * @param {string} key The name's key for the folder - * @param {Object} [object={}] An object containing all the SchemaFolders/SchemaPieces literals for this folder + * @param {Object} options An object containing the options for the new piece or folder. Check {@tutorial UnderstandingSchemaFolders} * @param {boolean} [force=true] Whether this function call should modify all entries from the database - * @returns {Promise} + * @returns {SchemaFolder} + * @example + * // Add a new SchemaPiece + * SchemaFolder.add('modlog', { + * type: 'TextChannel' + * }); + * + * // Add an empty new SchemaFolder + * SchemaFolder.add('channels'); + * + * // Optionally, you can set the type Folder, + * // if no type is set, 'Folder' will be implied. + * SchemaFolder.add('channels', { + * type: 'Folder' + * }); + * + * // Add a new SchemaFolder with a modlog key inside + * SchemaFolder.add('channels', { + * modlog: { + * type: 'TextChannel' + * } + * }); */ - async addFolder(key, object = {}, force = true) { - if (this.hasKey(key)) throw `The key ${key} already exists in the current schema.`; - if (typeof this[key] !== 'undefined') throw `The key ${key} conflicts with a property of Schema.`; - - const folder = this._addKey(key, object, SchemaFolder); + async add(key, options = {}, force = true) { + if (this.has(key)) throw new Error(`The key ${key} already exists in the current schema.`); + if (typeof this[key] !== 'undefined') throw new Error(`The key ${key} conflicts with a property of Schema.`); + if (!options || !isObject(options)) throw new Error(`The options object is required.`); + if (typeof options.type !== 'string' || options.type.toLowerCase() === 'folder') options.type = 'Folder'; + + // Create the piece and save the current schema + const piece = this._add(key, options, options.type === 'Folder' ? SchemaFolder : SchemaPiece); await fs.outputJSONAtomic(this.gateway.filePath, this.gateway.schema.toJSON()); if (this.gateway.sql) { - if (folder.keyArray.length > 0) { - if (typeof this.gateway.provider.addColumn === 'function') await this.gateway.provider.addColumn(this.gateway.type, folder.getSQL()); - else throw new Error('The method \'addColumn\' in your provider is required in order to add new columns.'); + if (piece.type !== 'Folder' || piece.keyArray.length) { + await this.gateway.provider.addColumn(this.gateway.type, piece.type === 'Folder' ? + piece.getSQL() : piece.sql[1]); } - } else if (force || this.gateway.type === 'clientStorage') { - await this.force('add', key, folder); + } else if (force || (this.gateway.type === 'clientStorage' && this.client.shard)) { + await this.force('add', key, piece); } - await this._shardSyncSchema(folder, 'add', force); - if (this.client.listenerCount('schemaKeyAdd')) this.client.emit('schemaKeyAdd', folder); + await this._shardSyncSchema(piece, 'add', force); + this.client.emit('schemaKeyAdd', piece); return this.gateway.schema; } /** - * Remove a nested folder. + * Remove a key * @since 0.5.0 - * @param {string} key The folder's name to remove + * @param {string} key The key's name to remove * @param {boolean} [force=true] Whether this function call should modify all entries from the database - * @returns {Promise} + * @returns {SchemaFolder} */ - async removeFolder(key, force = true) { - if (this.hasKey(key) === false) throw new Error(`The key ${key} does not exist in the current schema.`); - if (this[key].type !== 'Folder') throw new Error(`The key ${key} is not Folder type.`); + async remove(key, force = true) { + if (!this.has(key)) throw new Error(`The key ${key} does not exist in the current schema.`); - const folder = this[key]; - this._removeKey(key); + // Get the key, remove it from the configs and update the persistent schema + const piece = this._remove(key); await fs.outputJSONAtomic(this.gateway.filePath, this.gateway.schema.toJSON()); + // A SQL database has the advantage of being able to update all keys along the schema, so force is ignored if (this.gateway.sql) { - if (folder.keyArray.length > 0) { - if (typeof this.gateway.provider.removeColumn === 'function') await this.gateway.provider.removeColumn(this.gateway.type, folder.getKeys()); - else throw new Error('The method \'removeColumn\' in your provider is required in order to remove columns.'); + if (piece.type !== 'Folder' || (piece.type === 'Folder' && piece.keyArray.length > 0)) { + await this.gateway.provider.removeColumn(this.gateway.type, piece.type === 'Folder' ? + [...piece.keys(true)] : key); } - } else if (force || this.gateway.type === 'clientStorage') { - await this.force('delete', key, folder); + } else if (force || (this.gateway.type === 'clientStorage' && this.client.shard)) { + // If force, or if the gateway is clientStorage, it should update all entries + await this.force('delete', key, piece); } - await this._shardSyncSchema(folder, 'delete', force); - if (this.client.listenerCount('schemaKeyRemove')) this.client.emit('schemaKeyRemove', folder); + await this._shardSyncSchema(piece, 'delete', force); + this.client.emit('schemaKeyRemove', piece); return this.gateway.schema; } @@ -143,78 +165,29 @@ class SchemaFolder extends Schema { * @param {string} key The key to check * @returns {boolean} */ - hasKey(key) { - return this.keys.has(key); - } - - /** - * Add a new key to this folder. - * @since 0.5.0 - * @param {string} key The name for the key - * @param {SchemaFolderAddOptions} options The key's options to apply - * @param {boolean} [force=true] Whether this function call should modify all entries from the database - * @returns {Promise} - */ - async addKey(key, options, force = true) { - this._addKey(key, this._verifyKeyOptions(key, options), SchemaPiece); - await fs.outputJSONAtomic(this.gateway.filePath, this.gateway.schema.toJSON()); - - if (this.gateway.sql) { - if (typeof this.gateway.provider.addColumn === 'function') await this.gateway.provider.addColumn(this.gateway.type, key, this[key].sql[1]); - else throw new Error('The method \'addColumn\' in your provider is required in order to add new columns.'); - } else if (force || this.gateway.type === 'clientStorage') { - await this.force('add', key, this[key]); - } - - await this._shardSyncSchema(this[key], 'add', force); - if (this.client.listenerCount('schemaKeyAdd')) this.client.emit('schemaKeyAdd', this[key]); - return this.gateway.schema; - } - - /** - * Remove a key from this folder. - * @since 0.5.0 - * @param {string} key The key's name to remove - * @param {boolean} [force=true] Whether this function call should modify all entries from the database - * @returns {Promise} - */ - async removeKey(key, force = true) { - if (this.hasKey(key) === false) throw `The key ${key} does not exist in the current schema.`; - const schemaPiece = this[key]; - this._removeKey(key); - await fs.outputJSONAtomic(this.gateway.filePath, this.gateway.schema.toJSON()); - - if (this.gateway.sql) { - if (typeof this.gateway.provider.removeColumn === 'function') await this.gateway.provider.removeColumn(this.gateway.type, key); - else throw new Error('The method \'removeColumn\' in your provider is required in order to remove columns.'); - } else if (force) { - await this.force('delete', key, schemaPiece); - } - - await this._shardSyncSchema(schemaPiece, 'delete', force); - if (this.client.listenerCount('schemaKeyRemove')) this.client.emit('schemaKeyRemove', schemaPiece); - return this.gateway.schema; + has(key) { + return this.keyArray.includes(key); } /** * Modifies all entries from the database. * @since 0.5.0 - * @param {('add'|'edit'|'delete')} action The action to perform + * @param {('add'|'delete')} action The action to perform * @param {string} key The key * @param {(SchemaPiece|SchemaFolder)} piece The SchemaPiece instance to handle * @returns {Promise<*>} * @private */ force(action, key, piece) { - if (!(piece instanceof SchemaPiece) && !(piece instanceof SchemaFolder)) throw new TypeError(`'schemaPiece' must be an instance of 'SchemaPiece' or an instance of 'SchemaFolder'.`); + if (!(piece instanceof SchemaPiece) && !(piece instanceof SchemaFolder)) { + throw new TypeError(`'schemaPiece' must be an instance of 'SchemaPiece' or an instance of 'SchemaFolder'.`); + } - const values = this.gateway.cache.getValues(this.gateway.type); const path = piece.path.split('.'); - if (action === 'add' || action === 'edit') { - const defValue = this.defaults[key]; - for (let i = 0; i < values.length; i++) { - let value = values[i]; + if (action === 'add') { + const defValue = piece.type === 'Folder' ? piece.defaults : piece.default; + for (let value of this.gateway.cache.values()) { for (let j = 0; j < path.length - 1; j++) value = value[path[j]]; value[path[path.length - 1]] = deepClone(defValue); } @@ -222,8 +195,7 @@ class SchemaFolder extends Schema { } if (action === 'delete') { - for (let i = 0; i < values.length; i++) { - let value = values[i]; + for (let value of this.gateway.cache.values()) { for (let j = 0; j < path.length - 1; j++) value = value[path[j]]; delete value[path[path.length - 1]]; } @@ -233,40 +205,6 @@ class SchemaFolder extends Schema { throw new TypeError(`Action must be either 'add' or 'delete'. Got: ${action}`); } - /** - * Get a list. - * @since 0.5.0 - * @param {KlasaMessage} msg The Message instance - * @returns {string} - */ - getList(msg) { - const array = []; - const folders = []; - const keys = {}; - let longest = 0; - for (const key of this.keyArray) { - if (this[key].type === 'Folder') { - folders.push(`// ${key}`); - } else if (this[key].configurable) { - if (!(this[key].type in keys)) keys[this[key].type] = []; - if (key.length > longest) longest = key.length; - keys[this[key].type].push(key); - } - } - const keysTypes = Object.keys(keys); - if (folders.length === 0 && keysTypes.length === 0) return ''; - if (folders.length) array.push('= Folders =', ...folders.sort(), ''); - if (keysTypes.length) { - for (const keyType of keysTypes.sort()) { - keys[keyType].sort(); - array.push(`= ${toTitleCase(keyType)}s =`); - for (let i = 0; i < keys[keyType].length; i++) array.push(`${keys[keyType][i].padEnd(longest)} :: ${this[keys[keyType][i]].resolveString(msg)}`); - array.push(''); - } - } - return array.join('\n'); - } - /** * Get a JSON object with all the default values. * @since 0.5.0 @@ -296,42 +234,6 @@ class SchemaFolder extends Schema { return array; } - /** - * Get all the pathes from this schema's children. - * @since 0.5.0 - * @param {string[]} [array=[]] The array to push. - * @returns {string[]} - */ - getKeys(array = []) { - for (const key of this.keyArray) { - if (this[key].type === 'Folder') this[key].getKeys(array); - else array.push(this[key].path); - } - return array; - } - - /** - * Get all the SchemaPieces instances from this schema's children. Used for SQL. - * @since 0.5.0 - * @param {string[]} [array=[]] The array to push. - * @returns {SchemaPiece[]} - */ - getValues(array = []) { - for (const key of this.keyArray) { - if (this[key].type === 'Folder') this[key].getValues(array); - else array.push(this[key]); - } - return array; - } - - /** - * @since 0.5.0 - * @returns {string} - */ - resolveString() { - return this.toString(); - } - /** * Add a key to the instance. * @since 0.5.0 @@ -341,33 +243,33 @@ class SchemaFolder extends Schema { * @returns {(SchemaFolder|SchemaPiece)} * @private */ - _addKey(key, options, Piece) { - if (this.hasKey(key)) throw new Error(`The key '${key}' already exists.`); - const piece = new Piece(this.client, this.gateway, options, this, key); - this[key] = piece; - this.defaults[key] = piece.type === 'Folder' ? piece.defaults : options.default; - - this.keys.add(key); - this.keyArray.push(key); - this.keyArray.sort((a, b) => a.localeCompare(b)); + _add(key, options, Piece) { + if (this.has(key)) throw new Error(`The key '${key}' already exists.`); + this[key] = new Piece(this.client, this.gateway, options, this, key); - return piece; + const index = this.keyArray.findIndex(entry => entry.localeCompare(key)); + if (index === -1) this.keyArray.push(key); + else this.keyArray.splice(index, 0, key); + + return this[key]; } /** * Remove a key from the instance. * @since 0.5.0 * @param {string} key The name of the key + * @returns {(SchemaFolder|SchemaPiece)} * @private */ - _removeKey(key) { + _remove(key) { const index = this.keyArray.indexOf(key); if (index === -1) throw new Error(`The key '${key}' does not exist.`); - this.keys.delete(key); this.keyArray.splice(index, 1); + const piece = this[key]; delete this[key]; - delete this.defaults[key]; + + return piece; } /** @@ -383,40 +285,11 @@ class SchemaFolder extends Schema { await this.client.shard.broadcastEval(` if (this.shard.id !== ${this.client.shard.id}) { this.gateways.${this.gateway.type}._shardSync( - ${JSON.stringify(piece.path.split('.'))}, ${JSON.stringify(piece)}, ${action}, ${force}); + ${JSON.stringify(piece.path.split('.'))}, ${JSON.stringify(piece)}, '${action}', ${force}); } `); } - /** - * Verifies the key add options. - * @since 0.5.0 - * @param {string} key The name for the key - * @param {SchemaFolderAddOptions} options The key's options to apply - * @returns {addOptions} - * @private - */ - _verifyKeyOptions(key, options) { - if (this.hasKey(key)) throw `The key ${key} already exists in the current schema.`; - if (typeof this[key] !== 'undefined') throw `The key ${key} conflicts with a property of Schema.`; - if (!options) throw 'You must pass an options argument to this method.'; - if (typeof options.type !== 'string') throw 'The option type is required and must be a string.'; - options.type = options.type.toLowerCase(); - if (!this.client.gateways.types.includes(options.type)) throw `The type ${options.type} is not supported.`; - if (typeof options.min !== 'undefined' && isNaN(options.min)) throw 'The option min must be a number.'; - if (typeof options.max !== 'undefined' && isNaN(options.max)) throw 'The option max must be a number.'; - if (typeof options.array !== 'undefined' && typeof options.array !== 'boolean') throw 'The option array must be a boolean.'; - if (typeof options.configurable !== 'undefined' && typeof options.configurable !== 'boolean') throw 'The option configurable must be a boolean.'; - if (options.array) { - if (typeof options.default === 'undefined') options.default = []; - else if (!Array.isArray(options.default)) throw 'The option default must be an array if the array option is set to true.'; - } else { - if (typeof options.default === 'undefined') options.default = options.type === 'boolean' ? false : null; - options.array = false; - } - return options; - } - /** * Method called in initialization to populate the instance with the keys from the schema. * @since 0.5.0 @@ -434,13 +307,10 @@ class SchemaFolder extends Schema { if (object[key].type === 'Folder') { const folder = new SchemaFolder(this.client, this.gateway, object[key], this, key); this[key] = folder; - this.defaults[key] = folder.defaults; } else { const piece = new SchemaPiece(this.client, this.gateway, object[key], this, key); this[key] = piece; - this.defaults[key] = piece.default; } - this.keys.add(key); this.keyArray.push(key); } this.keyArray.sort((a, b) => a.localeCompare(b)); @@ -449,10 +319,79 @@ class SchemaFolder extends Schema { return true; } + /** + * Returns a new Iterator object that contains the `[key, value]` pairs for each element contained in this folder. + * Identical to [Map.entries()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries) + * @since 0.5.0 + * @param {boolean} recursive Whether the iteration should be recursive + * @yields {Array} + */ + *entries(recursive = false) { + if (recursive) { + for (const key of this.keyArray) { + if (this[key].type === 'Folder') yield* this[key].entries(true); + else yield [key, this[key]]; + } + } else { + for (const key of this.keyArray) yield [key, this[key]]; + } + } + + /** + * Returns a new Iterator object that contains the values for each element contained in this folder. + * Identical to [Map.values()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values) + * @since 0.5.0 + * @param {boolean} recursive Whether the iteration should be recursive + * @yields {(SchemaFolder|SchemaPiece)} + */ + *values(recursive = false) { + if (recursive) { + for (const key of this.keyArray) { + if (this[key].type === 'Folder') yield* this[key].values(true); + else yield this[key]; + } + } else { + for (const key of this.keyArray) yield this[key]; + } + } + + /** + * Returns a new Iterator object that contains the keys for each element contained in this folder. + * Identical to [Map.keys()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys) + * @since 0.5.0 + * @param {boolean} recursive Whether the iteration should be recursive + * @yields {string} + */ + *keys(recursive = false) { + if (recursive) { + for (const key of this.keyArray) { + if (this[key].type === 'Folder') yield* this[key].keys(true); + else yield key; + } + } else { + for (const key of this.keyArray) yield key; + } + } + + /** + * Returns a new Iterator object that contains the `[key, value]` pairs for each element contained in this folder. + * Identical to [Map.entries()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries) + * @name @@iterator + * @since 0.5.0 + * @method + * @instance + * @generator + * @returns {Iterator>} + */ + + [Symbol.iterator]() { + return this.entries(); + } + /** * Get a JSON object containing all the objects from this schema's children. * @since 0.5.0 - * @returns {any} + * @returns {Object} */ toJSON() { return Object.assign({ type: 'Folder' }, ...this.keyArray.map(key => ({ [key]: this[key].toJSON() }))); diff --git a/src/lib/settings/SchemaPiece.js b/src/lib/settings/SchemaPiece.js index f20fb12167..2d73c1821b 100644 --- a/src/lib/settings/SchemaPiece.js +++ b/src/lib/settings/SchemaPiece.js @@ -3,19 +3,18 @@ const Schema = require('./Schema'); const fs = require('fs-nextra'); /** - * You should never create an instance of this class. Use {@link SchemaFolder#addKey} instead. + * You should never create an instance of this class. Use {@link SchemaFolder#add} instead. * The SchemaPiece class that contains the data for a key and several helpers. */ class SchemaPiece extends Schema { /** - * @typedef {Object} SchemaPieceModifyOptions + * @typedef {Object} SchemaPieceEditOptions * @property {*} [default] The new default value * @property {number} [min] The new minimum range value * @property {number} [max] The new maximum range value * @property {boolean} [configurable] The new configurable value * @property {string} [sql] The new sql datatype - * @memberof SchemaPiece */ /** @@ -27,14 +26,13 @@ class SchemaPiece extends Schema { * @property {string} sql A tuple containing the name of the column and its data type * @property {boolean} array Whether the key should be stored as Array or not * @property {boolean} configurable Whether the key should be configurable by the config command or not - * @memberof SchemaPiece */ /** * @since 0.5.0 * @param {KlasaClient} client The client which initialized this instance * @param {Gateway} gateway The Gateway that manages this schema instance - * @param {AddOptions} options The object containing the properties for this schema instance + * @param {SchemaFolderAddOptions} options The object containing the properties for this schema instance * @param {SchemaFolder} parent The parent which holds this instance * @param {string} key The name of the key */ @@ -55,7 +53,7 @@ class SchemaPiece extends Schema { * @type {boolean} * @name SchemaPiece#array */ - this.array = typeof options.array !== 'undefined' ? options.array : false; + this.array = 'array' in options ? options.array : Array.isArray(options.default); /** * What this key should provide by default. @@ -63,7 +61,7 @@ class SchemaPiece extends Schema { * @type {*} * @name SchemaPiece#default */ - this.default = typeof options.default !== 'undefined' ? options.default : this.type === 'boolean' ? false : null; + this.default = 'default' in options ? options.default : this._generateDefault(); /** * The minimum value for this key. @@ -71,7 +69,7 @@ class SchemaPiece extends Schema { * @type {?number} * @name SchemaPiece#min */ - this.min = typeof options.min !== 'undefined' && isNaN(options.min) === false ? options.min : null; + this.min = 'min' in options ? options.min : null; /** * The maximum value for this key. @@ -79,7 +77,7 @@ class SchemaPiece extends Schema { * @type {?number} * @name SchemaPiece#max */ - this.max = typeof options.max !== 'undefined' && isNaN(options.max) === false ? options.max : null; + this.max = 'max' in options ? options.max : null; /** * A tuple of strings containing the path and the datatype. @@ -87,7 +85,7 @@ class SchemaPiece extends Schema { * @type {string[]} * @name SchemaPiece#sql */ - this.sql = [this.path]; + this.sql = [this.path, null]; /** * Whether this key should be configureable by the config command. When type is any, this key defaults to false. @@ -95,7 +93,7 @@ class SchemaPiece extends Schema { * @type {boolean} * @name SchemaPiece#configurable */ - this.configurable = typeof options.configurable !== 'undefined' ? options.configurable : this.type !== 'any'; + this.configurable = 'configurable' in options ? options.configurable : this.type !== 'any'; /** * The validator function for this instance. @@ -126,56 +124,23 @@ class SchemaPiece extends Schema { * @since 0.5.0 * @param {string} value The value to parse * @param {KlasaGuild} guild A Guild instance required for the resolver to work - * @returns {Promise<*>} + * @returns {*} */ async parse(value, guild) { const resolved = await this.gateway.resolver[this.type](value, guild, this.key, { min: this.min, max: this.max }); - if (this.validator) this.validator(resolved, guild); + if (this.validator) await this.validator(resolved, guild); return resolved; } - /** - * Resolve a string. - * @since 0.5.0 - * @param {KlasaMessage} msg The Message to use - * @returns {string} - */ - resolveString(msg) { - const value = this.constructor._resolveConfigs(this.gateway.type, msg).get(this.path); - if (value === null) return 'Not set'; - - let resolver = (val) => val; - switch (this.type) { - case 'Folder': resolver = () => 'Folder'; - break; - case 'user': resolver = (val) => (this.client.users.get(val) || { username: val && val.username ? val.username : val }).username; - break; - case 'textchannel': - case 'voicechannel': - case 'channel': resolver = (val) => (msg.guild.channels.get(val) || { name: val && val.name ? val.name : val }).name; - break; - case 'role': resolver = (val) => (msg.guild.roles.get(val) || { name: val && val.name ? val.name : val }).name; - break; - case 'guild': resolver = (val) => val && val.name ? val.name : val; - break; - case 'boolean': resolver = (val) => val === true ? 'Enabled' : 'Disabled'; - break; - // no default - } - - if (this.array && Array.isArray(value)) return value.length > 0 ? `[ ${value.map(resolver).join(' | ')} ]` : 'None'; - return resolver(value); - } - /** * Modify this SchemaPiece's properties. * @since 0.5.0 - * @param {SchemaPieceModifyOptions} options The new options - * @returns {Promise} + * @param {SchemaPieceEditOptions} options The new options + * @returns {this} */ - async modify(options) { + async edit(options) { // Check if the 'options' parameter is an object. - if (!isObject(options)) throw new TypeError(`SchemaPiece#modify expected an object as a parameter. Got: ${typeof options}`); + if (!isObject(options)) throw new TypeError(`SchemaPiece#edit expected an object as a parameter. Got: ${typeof options}`); const edited = new Set(); if (typeof options.sql === 'string' && this.sql[1] !== options.sql) { @@ -216,6 +181,18 @@ class SchemaPiece extends Schema { return this; } + /** + * Generate a default value if none is given + * @since 0.5.0 + * @returns {(Array<*>|false|null)} + * @private + */ + _generateDefault() { + if (this.array) return []; + if (this.type === 'boolean') return false; + return null; + } + /** * Checks if options.type is valid. * @since 0.5.0 @@ -225,7 +202,7 @@ class SchemaPiece extends Schema { */ _schemaCheckType(type) { if (typeof type !== 'string') throw new TypeError(`[KEY] ${this} - Parameter type must be a string.`); - if (!this.client.gateways.types.includes(type)) throw new TypeError(`[KEY] ${this} - ${type} is not a valid type.`); + if (!this.client.gateways.types.has(type)) throw new TypeError(`[KEY] ${this} - ${type} is not a valid type.`); } /** @@ -242,7 +219,7 @@ class SchemaPiece extends Schema { /** * Checks if options.default is valid. * @since 0.5.0 - * @param {AddOptions} options The options to validate + * @param {SchemaFolderAddOptions} options The options to validate * @throws {TypeError} * @private */ @@ -311,23 +288,18 @@ class SchemaPiece extends Schema { /** * Check if the key is properly configured. * @since 0.5.0 - * @param {AddOptions} options The options to parse + * @param {SchemaFolderAddOptions} options The options to parse * @returns {true} * @throws {TypeError} * @private */ _init(options) { if (this._inited) throw new TypeError(`[INIT] ${this} - Is already init. Aborting re-init.`); + this._inited = true; + // Check if the 'options' parameter is an object. if (!isObject(options)) throw new TypeError(`SchemaPiece#init expected an object as a parameter. Got: ${typeof options}`); - this._schemaCheckType(this.type); - this._schemaCheckArray(this.array); - this._schemaCheckDefault(this); - this._schemaCheckLimits(this.min, this.max); - this._schemaCheckConfigurable(this.configurable); - this.sql[1] = this._generateSQLDatatype(options.sql); - this._inited = true; return true; } @@ -373,23 +345,6 @@ class SchemaPiece extends Schema { return ''; } - /** - * Gets a configuration instance from KlasaMessage depending on the schema.gateway type. - * @since 0.5.0 - * @param {string} type The type of gateway - * @param {KlasaMessage} msg The message context to resolve from - * @returns {Configuration} - * @private - */ - static _resolveConfigs(type, msg) { - switch (type) { - case 'users': return msg.author.configs; - case 'guilds': return msg.guildConfigs; - case 'clientStorage': return msg.client.configs; - default: return null; - } - } - } module.exports = SchemaPiece; diff --git a/src/lib/structures/Command.js b/src/lib/structures/Command.js index 31415c4759..786de70bd6 100644 --- a/src/lib/structures/Command.js +++ b/src/lib/structures/Command.js @@ -32,7 +32,6 @@ class Command extends Piece { * @property {boolean} [subcommands=false] Whether to enable sub commands or not * @property {string} [usage=''] The usage string for the command * @property {?string} [usageDelim=undefined] The string to delimit the command input for usage - * @memberof Command */ /** @@ -280,7 +279,7 @@ class Command extends Piece { * @since 0.0.1 * @param {KlasaMessage} msg The command message mapped on top of the message used to trigger this command * @param {any[]} params The fully resolved parameters based on your usage / usageDelim - * @returns {Promise} You should return the response message whenever possible + * @returns {KlasaMessage|KlasaMessage[]} You should return the response message whenever possible * @abstract */ async run(msg) { diff --git a/src/lib/structures/Event.js b/src/lib/structures/Event.js index e5abe86fd4..d2095f2b81 100644 --- a/src/lib/structures/Event.js +++ b/src/lib/structures/Event.js @@ -10,7 +10,6 @@ class Event extends Piece { /** * @typedef {PieceOptions} EventOptions - * @memberof Event */ /** diff --git a/src/lib/structures/Extendable.js b/src/lib/structures/Extendable.js index 9461a1b1dc..4f79ceaa91 100644 --- a/src/lib/structures/Extendable.js +++ b/src/lib/structures/Extendable.js @@ -12,7 +12,6 @@ class Extendable extends Piece { /** * @typedef {PieceOptions} ExtendableOptions * @property {boolean} [klasa=false] If the extendable is for Klasa instead of Discord.js - * @memberof Extendable */ /** diff --git a/src/lib/structures/Finalizer.js b/src/lib/structures/Finalizer.js index a267aa5d9b..1199988c2b 100644 --- a/src/lib/structures/Finalizer.js +++ b/src/lib/structures/Finalizer.js @@ -10,7 +10,6 @@ class Finalizer extends Piece { /** * @typedef {PieceOptions} FinalizerOptions - * @memberof Finalizer */ /** diff --git a/src/lib/structures/Inhibitor.js b/src/lib/structures/Inhibitor.js index f3cd7e132e..3c59175cd8 100644 --- a/src/lib/structures/Inhibitor.js +++ b/src/lib/structures/Inhibitor.js @@ -11,7 +11,6 @@ class Inhibitor extends Piece { /** * @typedef {PieceOptions} InhibitorOptions * @property {boolean} [spamProtection=false] If this inhibitor is meant for spamProtection (disables the inhibitor while generating help) - * @memberof Inhibitor */ /** @@ -38,7 +37,7 @@ class Inhibitor extends Piece { * @since 0.0.1 * @param {KlasaMessage} msg The message that triggered this inhibitor * @param {Command} cmd The command to run - * @returns {Promise<(void|string)>} + * @returns {(void|string)} * @abstract */ async run() { diff --git a/src/lib/structures/Language.js b/src/lib/structures/Language.js index 235014c560..4662e44413 100644 --- a/src/lib/structures/Language.js +++ b/src/lib/structures/Language.js @@ -13,7 +13,6 @@ class Language extends Piece { /** * @typedef {PieceOptions} LanguageOptions - * @memberof Language */ /** diff --git a/src/lib/structures/Monitor.js b/src/lib/structures/Monitor.js index 6b9b851fbc..5fee9833e9 100644 --- a/src/lib/structures/Monitor.js +++ b/src/lib/structures/Monitor.js @@ -14,7 +14,6 @@ class Monitor extends Piece { * @property {boolean} [ignoreSelf=true] Whether the monitor ignores itself or not * @property {boolean} [ignoreOthers=true] Whether the monitor ignores others or not * @property {boolean} [ignoreWebhooks=true] Whether the monitor ignores webhooks or not - * @memberof Monitor */ /** diff --git a/src/lib/structures/Provider.js b/src/lib/structures/Provider.js index 79e4ce149f..bad5259581 100644 --- a/src/lib/structures/Provider.js +++ b/src/lib/structures/Provider.js @@ -11,7 +11,6 @@ class Provider extends Piece { /** * @typedef {PieceOptions} ProviderOptions * @property {boolean} [sql=false] If the provider provides to a sql data source - * @memberof Provider */ /** diff --git a/src/lib/structures/Task.js b/src/lib/structures/Task.js index b7a065d33d..bb5cc5833f 100644 --- a/src/lib/structures/Task.js +++ b/src/lib/structures/Task.js @@ -10,17 +10,16 @@ class Task extends Piece { /** * @typedef {PieceOptions} TaskOptions - * @memberof Task */ /** * The run method to be overwritten in actual Task pieces * @since 0.5.0 * @param {*} data The data from the ScheduledTask instance - * @returns {Promise} + * @returns {void} * @abstract */ - run() { + async run() { // Defined in extension Classes } diff --git a/src/lib/structures/base/Piece.js b/src/lib/structures/base/Piece.js index c647d87019..62e1ab94e0 100644 --- a/src/lib/structures/base/Piece.js +++ b/src/lib/structures/base/Piece.js @@ -18,7 +18,6 @@ class Piece { * @typedef {Object} PieceOptions * @property {string} [name=theFileName] The name of the event * @property {boolean} [enabled=true] Whether the event is enabled or not - * @memberof Piece */ /** diff --git a/src/lib/usage/TextPrompt.js b/src/lib/usage/TextPrompt.js index 6ab758437f..77911a53d9 100644 --- a/src/lib/usage/TextPrompt.js +++ b/src/lib/usage/TextPrompt.js @@ -11,7 +11,6 @@ class TextPrompt { * @property {number} [promptLimit=Infinity] The number of re-prompts before this TextPrompt gives up * @property {number} [promptTime=30000] The time-limit for re-prompting * @property {boolean} [quotedStringSupport=false] Whether this prompt should respect quoted strings - * @memberof TextPrompt */ /** @@ -132,7 +131,7 @@ class TextPrompt { * Runs the custom prompt. * @since 0.5.0 * @param {string} prompt The message to initially prompt with - * @returns {Promise} The parameters resolved + * @returns {any[]} The parameters resolved */ async run(prompt) { const message = await this.message.prompt(prompt, this.promptTime); @@ -144,7 +143,7 @@ class TextPrompt { * Collects missing required arguments. * @since 0.5.0 * @param {string} prompt The reprompt error - * @returns {Promise} + * @returns {any[]} * @private */ async reprompt(prompt) { @@ -171,7 +170,7 @@ class TextPrompt { /** * Collects repeating arguments. * @since 0.5.0 - * @returns {Promise} + * @returns {any[]} * @private */ async repeatingPrompt() { @@ -199,7 +198,7 @@ class TextPrompt { /** * Validates and resolves args into parameters * @since 0.0.1 - * @returns {Promise} The resolved parameters + * @returns {any[]} The resolved parameters * @private */ async validateArgs() { @@ -222,7 +221,7 @@ class TextPrompt { * Validates and resolves args into parameters, when multiple types of usage is defined * @since 0.0.1 * @param {number} index The id of the possible usage currently being checked - * @returns {Promise} The resolved parameters + * @returns {any[]} The resolved parameters * @private */ async multiPossibles(index) { @@ -275,7 +274,7 @@ class TextPrompt { * Decides if the prompter should reprompt or throw the error found while validating. * @since 0.5.0 * @param {string} err The error found while validating - * @returns {Promise} + * @returns {any[]} * @private */ async handleError(err) { diff --git a/src/lib/util/Colors.js b/src/lib/util/Colors.js index 7dc9d26f7b..efb3a8dd34 100644 --- a/src/lib/util/Colors.js +++ b/src/lib/util/Colors.js @@ -11,19 +11,16 @@ class Colors { * @property {(string|string[])} style The style or styles to apply * @property {ColorsFormatType} background The format for the background * @property {ColorsFormatType} text The format for the text - * @memberof Colors */ /** * @typedef {(string|number|string[]|number[])} ColorsFormatType - * @memberof Colors */ /** * @typedef {Object} ColorsFormatData * @property {string[]} opening The opening format data styles * @property {string[]} closing The closing format data styles - * @memberof Colors * @private */ diff --git a/src/lib/util/KlasaConsole.js b/src/lib/util/KlasaConsole.js index 8e06fbb227..d54ba416c1 100644 --- a/src/lib/util/KlasaConsole.js +++ b/src/lib/util/KlasaConsole.js @@ -17,7 +17,6 @@ class KlasaConsole extends Console { * @property {NodeJS.WritableStream} [stderr] The WritableStrwam for the error logs * @property {(boolean|string)} [timestamps] If false, it won't use timestamps. Otherwise it will use 'YYYY-MM-DD HH:mm:ss' if true or custom if string is given * @property {boolean} [useColor] Whether the timestamps should use colours - * @memberof KlasaConsole */ /** @@ -28,7 +27,6 @@ class KlasaConsole extends Console { * @property {KlasaConsoleColorObjects} verbose An object containing a message and time color object * @property {KlasaConsoleColorObjects} warn An object containing a message and time color object * @property {KlasaConsoleColorObjects} wtf An object containing a message and time Color Object - * @memberof KlasaConsole */ /** @@ -36,7 +34,6 @@ class KlasaConsole extends Console { * @property {string} [type='log'] The method from Console this color object should call * @property {KlasaConsoleMessageObject} message A message object containing colors and styles * @property {KlasaConsoleTimeObject} time A time object containing colors and styles - * @memberof KlasaConsole */ /** @@ -44,7 +41,6 @@ class KlasaConsole extends Console { * @property {KlasaConsoleColorTypes} background The background color. Can be a basic string like "red", a hex string, or a RGB array * @property {KlasaConsoleColorTypes} text The text color. Can be a basic string like "red", a hex string, or a RGB array * @property {KlasaConsoleStyleTypes} style A style string from KlasaConsoleStyleTypes - * @memberof KlasaConsole */ /** @@ -52,7 +48,6 @@ class KlasaConsole extends Console { * @property {KlasaConsoleColorTypes} background The background color. Can be a basic string like "red", a hex string, or a RGB array * @property {KlasaConsoleColorTypes} text The text color. Can be a basic string like "red", a hex string, a RGB array, or HSL array * @property {KlasaConsoleStyleTypes} style A style string from KlasaConsoleStyleTypes - * @memberof KlasaConsole */ /** @@ -75,7 +70,6 @@ class KlasaConsole extends Console { * @property {string} lightmagenta The light magenta colour * @property {string} lightcyan The light cyan colour * @property {string} white The white colour - * @memberof KlasaConsole */ /** @@ -88,7 +82,6 @@ class KlasaConsole extends Console { * @property {string} inverse Inverse colours style * @property {string} hidden Hidden text style * @property {string} strikethrough Strikethrough text style - * @memberof KlasaConsole */ /** diff --git a/src/lib/util/ReactionHandler.js b/src/lib/util/ReactionHandler.js index e4fead962f..68da972e60 100644 --- a/src/lib/util/ReactionHandler.js +++ b/src/lib/util/ReactionHandler.js @@ -8,8 +8,7 @@ class ReactionHandler extends ReactionCollector { /** * A single unicode character - * @typedef {string} emoji - * @memberof ReactionHandler + * @typedef {string} Emoji */ /** @@ -22,7 +21,6 @@ class ReactionHandler extends ReactionCollector { * @property {number} [maxEmojis] The maximum number of emojis to collect * @property {number} [maxUsers] The maximum number of users to react * @property {number} [time] The maximum amount of time before this RichMenu should expire - * @memberof RichMenu */ /** @@ -32,7 +30,7 @@ class ReactionHandler extends ReactionCollector { * @param {Function} filter A filter function to determine which emoji reactions should be handled * @param {ReactionHandlerOptions} options The options for this ReactionHandler * @param {(RichDisplay|RichMenu)} display The RichDisplay or RichMenu that this handler is for - * @param {emoji[]} emojis The emojis which should be used in this handler + * @param {Emoji[]} emojis The emojis which should be used in this handler */ constructor(msg, filter, options, display, emojis) { super(msg, filter, options); @@ -47,7 +45,7 @@ class ReactionHandler extends ReactionCollector { /** * An emoji to method map, to map custom emojis to static method names * @since 0.4.0 - * @type {Map} + * @type {Map} */ this.methodMap = new Map(Object.entries(this.display.emojis).map(([key, value]) => [value, key])); @@ -324,7 +322,7 @@ class ReactionHandler extends ReactionCollector { /** * The action to take when the "first" emoji is reacted * @since 0.4.0 - * @param {emoji[]} emojis The remaining emojis to react + * @param {Emoji[]} emojis The remaining emojis to react * @returns {null} * @private */ diff --git a/src/lib/util/RichDisplay.js b/src/lib/util/RichDisplay.js index baeaeeee62..76d2781034 100644 --- a/src/lib/util/RichDisplay.js +++ b/src/lib/util/RichDisplay.js @@ -6,22 +6,15 @@ const ReactionHandler = require('./ReactionHandler'); */ class RichDisplay { - /** - * A single unicode character - * @typedef {string} emoji - * @memberof RichDisplay - */ - /** * @typedef {Object} RichDisplayEmojisObject - * @property {emoji} first The emoji for the 'first' button - * @property {emoji} back The emoji for the 'back' button - * @property {emoji} forward The emoji for the 'forward' button - * @property {emoji} last The emoji for the 'last' button - * @property {emoji} jump The emoji for the 'jump' button - * @property {emoji} info The emoji for the 'info' button - * @property {emoji} stop The emoji for the 'stop' button - * @memberof RichDisplay + * @property {Emoji} first The emoji for the 'first' button + * @property {Emoji} back The emoji for the 'back' button + * @property {Emoji} forward The emoji for the 'forward' button + * @property {Emoji} last The emoji for the 'last' button + * @property {Emoji} jump The emoji for the 'jump' button + * @property {Emoji} info The emoji for the 'info' button + * @property {Emoji} stop The emoji for the 'stop' button */ /** @@ -36,7 +29,6 @@ class RichDisplay { * @property {number} [maxEmojis] The maximum number of emojis to collect * @property {number} [maxUsers] The maximum number of users to react * @property {number} [time] The maximum amount of time before this RichDisplay should expire - * @memberof RichDisplay */ /** @@ -226,11 +218,11 @@ class RichDisplay { /** * Determines the emojis to use in this display * @since 0.4.0 - * @param {emoji[]} emojis An array of emojis to use + * @param {Emoji[]} emojis An array of emojis to use * @param {boolean} stop Whether the stop emoji should be included * @param {boolean} jump Whether the jump emoji should be included * @param {boolean} firstLast Whether the first & last emojis should be included - * @returns {emoji[]} + * @returns {Emoji[]} * @private */ _determineEmojis(emojis, stop, jump, firstLast) { diff --git a/src/lib/util/RichMenu.js b/src/lib/util/RichMenu.js index 25b2a5636e..3140203908 100644 --- a/src/lib/util/RichMenu.js +++ b/src/lib/util/RichMenu.js @@ -7,31 +7,17 @@ const RichDisplay = require('./RichDisplay'); class RichMenu extends RichDisplay { /** - * A single unicode character - * @typedef {string} emoji - * @memberof RichMenu - */ - - /** - * @typedef {Object} RichMenuEmojisObject - * @property {emoji} first The emoji for the 'first' button - * @property {emoji} back The emoji for the 'back' button - * @property {emoji} forward The emoji for the 'forward' button - * @property {emoji} last The emoji for the 'last' button - * @property {emoji} jump The emoji for the 'jump' button - * @property {emoji} info The emoji for the 'info' button - * @property {emoji} stop The emoji for the 'stop' button - * @property {emoji} zero The emoji for the 'zero' button - * @property {emoji} one The emoji for the 'one' button - * @property {emoji} two The emoji for the 'two' button - * @property {emoji} three The emoji for the 'three' button - * @property {emoji} four The emoji for the 'four' button - * @property {emoji} five The emoji for the 'five' button - * @property {emoji} six The emoji for the 'six' button - * @property {emoji} seven The emoji for the 'seven' button - * @property {emoji} eight The emoji for the 'eight' button - * @property {emoji} nine The emoji for the 'nine' button - * @memberof RichMenu + * @typedef {RichDisplayEmojisObject} RichMenuEmojisObject + * @property {Emoji} zero The emoji for the 'zero' button + * @property {Emoji} one The emoji for the 'one' button + * @property {Emoji} two The emoji for the 'two' button + * @property {Emoji} three The emoji for the 'three' button + * @property {Emoji} four The emoji for the 'four' button + * @property {Emoji} five The emoji for the 'five' button + * @property {Emoji} six The emoji for the 'six' button + * @property {Emoji} seven The emoji for the 'seven' button + * @property {Emoji} eight The emoji for the 'eight' button + * @property {Emoji} nine The emoji for the 'nine' button */ /** @@ -39,7 +25,6 @@ class RichMenu extends RichDisplay { * @property {string} name The name of the option * @property {string} body The description of the option * @property {boolean} [inline=false] Whether the option should be inline - * @memberof RichMenu */ /** @@ -52,7 +37,6 @@ class RichMenu extends RichDisplay { * @property {number} [maxEmojis] The maximum number of emojis to collect * @property {number} [maxUsers] The maximum number of users to react * @property {number} [time] The maximum amount of time before this RichMenu should expire - * @memberof RichMenu */ /** @@ -134,11 +118,11 @@ class RichMenu extends RichDisplay { /** * Determines the emojis to use in this menu * @since 0.4.0 - * @param {emoji[]} emojis An array of emojis to use + * @param {Emoji[]} emojis An array of emojis to use * @param {boolean} stop Whether the stop emoji should be included * @param {boolean} jump Whether the jump emoji should be included * @param {boolean} firstLast Whether the first & last emojis should be included - * @returns {emoji[]} + * @returns {Emoji[]} * @private */ _determineEmojis(emojis, stop, jump, firstLast) { diff --git a/src/lib/util/Timestamp.js b/src/lib/util/Timestamp.js index 59df7ec4ee..a07b6a9a6a 100644 --- a/src/lib/util/Timestamp.js +++ b/src/lib/util/Timestamp.js @@ -9,7 +9,6 @@ class Timestamp { * @typedef {Object} TimestampObject * @property {string} type The type of the current variable * @property {string} [content] The content of the type. Only accessible if the type is 'literal' - * @memberof Timestamp */ /** diff --git a/src/lib/util/constants.js b/src/lib/util/constants.js index f5722aa858..fa9369cdfc 100644 --- a/src/lib/util/constants.js +++ b/src/lib/util/constants.js @@ -1,5 +1,4 @@ const { dirname } = require('path'); -const { Guild, User, Client } = require('discord.js'); exports.DEFAULTS = { @@ -129,44 +128,6 @@ exports.DEFAULTS = { }; -exports.GATEWAY_RESOLVERS = { - - GUILDS: async function validateGuild(guildResolvable) { - if (guildResolvable) { - let value; - - if (typeof guildResolvable === 'string' && /^\d{17,19}$/.test(guildResolvable)) value = this.client.guilds.get(guildResolvable); - else if (guildResolvable instanceof Guild) value = guildResolvable; - if (value) return value; - } - - throw new Error('The parameter expects either a Guild ID or a Guild Instance.'); - }, - - USERS: async function validateUser(userResolvable) { - if (userResolvable) { - let value; - - if (typeof userResolvable === 'string' && /^\d{17,19}$/.test(userResolvable)) value = await this.client.users.fetch(userResolvable); - else if (userResolvable instanceof User) value = userResolvable; - if (value) return value; - } - - throw new Error('The parameter expects either a User ID or a User Instance.'); - }, - - CLIENT_STORAGE: async function validateClient(clientResolvable) { - if (typeof clientResolvable === 'string' && clientResolvable === this.client.user.id) return this.client.user; - if (clientResolvable instanceof Client) return clientResolvable.user; - if (typeof clientResolvable === 'object' && - typeof clientResolvable.client !== 'undefined' && - clientResolvable.client instanceof Client) return clientResolvable.client.user; - - throw new Error('The parameter expects either a Client Instance.'); - } - -}; - exports.TIME = { SECOND: 1000, MINUTE: 1000 * 60, diff --git a/src/lib/util/util.js b/src/lib/util/util.js index 68a381f7a8..04f1828ad4 100644 --- a/src/lib/util/util.js +++ b/src/lib/util/util.js @@ -103,7 +103,7 @@ class Util { } if (Util.isObject(source)) { const output = {}; - for (const key in source) output[key] = source[key]; + for (const key in source) output[key] = Util.deepClone(source[key]); return output; } if (source instanceof Map || source instanceof WeakMap) { @@ -257,7 +257,7 @@ class Util { * @returns {string} */ static getDeepTypeSetOrArray(input, basic = Util.getTypeName(input)) { - if (!(input instanceof Array || input instanceof Set || input instanceof WeakSet)) return basic; + if (!(Array.isArray(input) || input instanceof Set || input instanceof WeakSet)) return basic; const types = new Set(); for (const value of input) { const type = Util.getDeepTypeName(value); @@ -294,6 +294,21 @@ class Util { } } + /** + * Get the identifier of a value. + * @since 0.5.0 + * @param {*} value The value to get the identifier from + * @returns {string} + */ + static getIdentifier(value) { + if (typeof value === 'string') return value; + if (Util.isObject(value)) { + if ('id' in value) return value.id; + if ('name' in value) return value.name; + } + return null; + } + /** * Turn a dotted path into a json object. * @since 0.5.0 @@ -312,6 +327,27 @@ class Util { return object; } + /** + * Compare if both arrays are equal + * @param {any[]} arr1 The first array to compare + * @param {any[]} arr2 The second array to compare + * @param {boolean} clone Whether this check should clone the second array + * @returns {boolean} + */ + static arraysEqual(arr1, arr2, clone = false) { + if (arr1 === arr2) return true; + if (arr1.length !== arr2.length) return false; + // Clone the array + if (clone) arr2 = arr2.slice(0); + + for (const item of arr1) { + const ind = arr2.indexOf(item); + if (ind !== -1) arr2.splice(ind, 1); + } + + return arr2.length === 0; + } + /** * Sets default properties on an object that aren't already specified. * @since 0.5.0 @@ -346,7 +382,6 @@ class Util { * @property {string|number} [killSignal='SIGTERM'] The kill signal * @property {number} [uid] Sets the user identity of the process * @property {number} [gid] Sets the group identity of the process - * @memberof Util */ /** diff --git a/src/providers/collection.js b/src/providers/collection.js deleted file mode 100644 index 2e792ecc29..0000000000 --- a/src/providers/collection.js +++ /dev/null @@ -1,154 +0,0 @@ -const { Provider } = require('klasa'); -const { Collection } = require('discord.js'); - -module.exports = class extends Provider { - - constructor(...args) { - super(...args, { - description: 'Allows you to use JSON functionality throughout Klasa', - cache: true - }); - this.database = new Collection(); - } - - /* Table methods */ - - /** - * Checks if a table exists. - * @param {string} table The name of the table you want to check - * @returns {boolean} - */ - hasTable(table) { - return this.database.has(table); - } - - /** - * Get the raw data from a table. - * @param {string} table The table to get the data from - * @returns {Collection} - */ - getTable(table) { - return this.database.get(table) || this.createTable(table).get(table); - } - - /** - * Creates a new table. - * @param {string} table The name for the new table - * @returns {Collection>} - */ - createTable(table) { - return this.database.set(table, new Collection()); - } - - /** - * Recursively deletes a table. - * @param {string} table The table's name to delete - * @returns {boolean} - */ - deleteTable(table) { - return this.database.delete(table); - } - - /* Entry methods */ - - /** - * Get all the values from a table. - * @param {string} table The name of the table to fetch from - * @returns {Object[]} - */ - getAll(table) { - return this.getTable(table); - } - - /** - * Get all the keys from a table. - * @param {string} table The name of the table to fetch the keys from - * @returns {string[]} - */ - getKeys(table) { - return [...this.getTable(table).keys()]; - } - - /** - * Get all the values from a table. - * @param {string} table The name of the table to fetch the values from - * @returns {any[]} - */ - getValues(table) { - return [...this.getTable(table).values()]; - } - - /** - * Get a entry from a table. - * @param {string} table The name of the table - * @param {string} entry The entry name - * @returns {?Object} - */ - get(table, entry) { - const collection = this.getTable(table); - return collection.get(entry) || null; - } - - /** - * Check if the entry exists. - * @param {string} table The name of the table - * @param {string} entry The entry name - * @returns {boolean} - */ - has(table, entry) { - return Boolean(this.get(table, entry)); - } - - /** - * Get a random entry from a table. - * @param {string} table The name of the table - * @returns {Object} - */ - getRandom(table) { - const array = this.getAll(table); - return array[Math.floor(Math.random() * array.length)]; - } - - /** - * Insert a new entry into a table. - * @param {string} table The name of the table - * @param {string} entry The entry name - * @param {Object} data The object with all properties you want to insert into the entry - * @returns {void} - */ - create(table, entry, data) { - const collection = this.getTable(table); - return collection.set(entry, data); - } - - set(...args) { - return this.create(...args); - } - - insert(...args) { - return this.create(...args); - } - - /** - * Update a entry from a table. - * @param {string} table The name of the table - * @param {string} entry The entry name - * @param {Object} data The object with all the properties you want to update - * @returns {Collection} - */ - update(table, entry, data) { - return this.getTable(table).set(entry, data); - } - - /** - * Delete a entry from the table. - * @param {string} table The name of the table - * @param {string} entry The entry name - * @returns {boolean} - */ - delete(table, entry) { - const collection = this.getTable(table); - return collection.delete(entry); - } - -}; diff --git a/typings/index.d.ts b/typings/index.d.ts index 08cb3ec205..2149ad5dac 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -25,10 +25,11 @@ declare module 'klasa' { Role, Snowflake, StringResolvable, - TextChannel as DiscordTextChannel, User as DiscordUser, UserResolvable, + TextChannel as DiscordTextChannel, VoiceChannel as DiscordVoiceChannel, + CategoryChannel as DiscordCategoryChannel, WebhookClient } from 'discord.js'; @@ -129,7 +130,7 @@ declare module 'klasa' { // SettingGateway Events public on(event: 'configCreateEntry', listener: (entry: Configuration) => void): this; public on(event: 'configDeleteEntry', listener: (entry: Configuration) => void): this; - public on(event: 'configUpdateEntry', listener: (oldEntry: Configuration, newEntry: Configuration, path: string | ConfigUpdateEntryMany) => void): this; + public on(event: 'configUpdateEntry', listener: (oldEntry: Configuration, newEntry: Configuration, path: string[]) => void): this; // Schema Events public on(event: 'schemaKeyAdd', listener: (key: SchemaFolder | SchemaPiece) => void): this; @@ -284,6 +285,10 @@ declare module 'klasa' { public readonly guild: KlasaGuild; } + export class KlasaCategoryChannel extends DiscordCategoryChannel { + public readonly guild: KlasaGuild; + } + export class KlasaDMChannel extends DiscordDMChannel { public readonly attachable: boolean; public readonly embedable: boolean; @@ -433,6 +438,7 @@ declare module 'klasa' { public user(data: any, guild: KlasaGuild, name: string): Promise; public user(input: KlasaUser | GuildMember | KlasaMessage | Snowflake): Promise; public voicechannel(data: any, guild: KlasaGuild, name: string): Promise; + public categorychannel(data: any, guild: KlasaGuild, name: string): Promise; public static maxOrMin(guild: KlasaGuild, value: number, min: number, max: number, name: string, suffix: string): boolean; } @@ -476,6 +482,8 @@ declare module 'klasa' { private _insert(task: ScheduledTask): ScheduledTask; private _clearInterval(): void; private _checkInterval(): void; + + public [Symbol.iterator](): Iterator; } export class ScheduledTask { @@ -507,30 +515,31 @@ declare module 'klasa' { public constructor(manager: Gateway, data: any); public readonly client: KlasaClient; public readonly gateway: Gateway; - public readonly type: string; public readonly id: string; private _existsInDB: boolean; private _syncStatus?: Promise; public get(key: string): any; + public get(key: string): T; public clone(): Configuration; - public resetConfiguration(): Promise; public sync(): Promise; public destroy(): Promise; - public reset(key: string, avoidUnconfigurable?: boolean): Promise; - public update(key: object, guild?: GatewayGuildResolvable): Promise; - public update(key: string, value?: any, options?: ConfigurationUpdateOptions): Promise; - public update(key: string, value?: any, guild?: GatewayGuildResolvable, options?: ConfigurationUpdateOptions): Promise; - - private _updateMany(object: any, guild?: GatewayGuildResolvable): Promise; - private _reset(key: string, guild: GatewayGuildResolvable, avoidUnconfigurable: boolean): Promise; - private _parseReset(key: string, guild: KlasaGuild, options: ConfigurationPathResult): Promise; - private _parseUpdateOne(key: string, value: any, guild: KlasaGuild, options: ConfigurationPathResult): Promise; - private _parseUpdateArray(action: 'add' | 'remove' | 'auto', key: string, value: any, guild: KlasaGuild, arrayPosition: number, options: ConfigurationPathResult): Promise; - private _parseUpdateMany(cache: any, object: any, schema: SchemaFolder, guild: KlasaGuild, list: ConfigurationUpdateManyResult, updateObject: object): void; - private _updateSingle(key: string, value: any, guild: KlasaGuild, options: ConfigurationUpdateOptions): Promise; - private _setValue(parsedID: string, path: SchemaPiece, route: string[]): Promise; + public reset(key?: string | string[], avoidUnconfigurable?: boolean): Promise; + public reset(key?: string | string[], guild?: KlasaGuild, avoidUnconfigurable?: boolean): Promise; + public update(key: object, guild?: GatewayGuildResolvable): Promise; + public update(key: string, value: any, guild?: GatewayGuildResolvable, options?: ConfigurationUpdateOptions): Promise; + public update(key: string[], value: any[], guild?: GatewayGuildResolvable, options?: ConfigurationUpdateOptions): Promise; + public list(msg: KlasaMessage, path: SchemaFolder | string): string; + public resolveString(msg: KlasaMessage, path: SchemaPiece | string): string; + + private _get(route: string | string[], piece?: boolean): object; + private _get(route: string | string[], piece?: boolean): T; + private _save(data: ConfigurationUpdateResult): Promise; + private _updateMany(object: any, guild?: GatewayGuildResolvable): Promise; + private _parseSingle(key: string, value: any, guild: KlasaGuild | null, options: ConfigurationUpdateOptions, list: ConfigurationUpdateResult): Promise; + private _parseUpdateMany(cache: any, object: any, schema: SchemaFolder, guild: KlasaGuild, list: ConfigurationUpdateResult): void; + private _setValueByPath(piece: SchemaPiece, parsedID: any): { updated: boolean, old: any }; private _patch(data: any): void; public toJSON(): object; @@ -539,30 +548,29 @@ declare module 'klasa' { private static _merge(data: any, folder: SchemaFolder | SchemaPiece): any; private static _clone(data: any, schema: SchemaFolder): any; private static _patch(inst: any, data: any, schema: SchemaFolder): void; - private static getIdentifier(value: any): any; } export class Gateway extends GatewayStorage { - private constructor(store: GatewayDriver, type: string, validateFunction: Function, schema: object, options: GatewayDriverAddOptions); + private constructor(store: GatewayDriver, type: string, schema: object, options: GatewayOptions); public store: GatewayDriver; - public options: GatewayDriverAddOptions; - public validate: Function; + public options: GatewayOptions; public defaultSchema: object; - public readonly cache: Provider; public readonly resolver: SettingResolver; + public readonly cache: Collection; public getEntry(input: string, create?: boolean): object | Configuration; public createEntry(input: string): Promise; public insertEntry(id: string, data?: object): Configuration; public deleteEntry(input: string): Promise; public sync(input?: object | string, download?: boolean): Promise; - public getPath(key?: string, options?: GatewayGetPathOptions): GatewayGetPathResult; + public getPath(key?: string, options?: GatewayGetPathOptions): GatewayGetPathResult | null; private init(download?: boolean): Promise; private _ready(): Promise>>; private _resolveGuild(guild: GatewayGuildResolvable): KlasaGuild; private _shardSync(path: string[], data: any, action: 'add' | 'delete' | 'update', force: boolean): Promise; + public toJSON(): GatewayJSON; public toString(): string; } @@ -570,9 +578,10 @@ declare module 'klasa' { private constructor(client: KlasaClient); public readonly client: KlasaClient; public resolver: SettingResolver; - public types: string[]; - public caches: string[]; + public types: Set; + public keys: Set; public ready: boolean; + private _queue: Map Gateway)>; public readonly guildsSchema: { prefix: SchemaPieceJSON, @@ -591,9 +600,14 @@ declare module 'klasa' { public users: Gateway; public clientStorage: Gateway; - public add(name: string, validateFunction: Function, schema?: object, options?: GatewayDriverAddOptions, download?: boolean): Promise; + public register(name: string, schema?: object, options?: GatewayDriverAddOptions): this; + public add(name: string, schema?: object, options?: GatewayDriverAddOptions, download?: boolean): Promise; + private _register(name: string, schema?: object, options?: GatewayDriverAddOptions): Gateway; private _ready(): Promise>>>; private _checkProvider(engine: string): string; + + public toJSON(): GatewayDriverJSON; + public toString(): string; } export abstract class GatewayStorage { @@ -615,7 +629,6 @@ declare module 'klasa' { private initSchema(): Promise; private parseEntry(entry: any): any; - private static throwError(guild: KlasaGuild, code: string | number, error: string | Error): string; private static _parseSQLValue(value: any, schemaPiece: SchemaPiece): any; } @@ -633,28 +646,27 @@ declare module 'klasa' { private constructor(client: KlasaClient, gateway: Gateway, object: any, parent: SchemaFolder, key: string); public readonly type: 'Folder'; public defaults: object; - public keys: Set; public keyArray: string[]; public readonly configurableKeys: string[]; - public addFolder(key: string, object?: object, force?: boolean): Promise; - public removeFolder(key: string, force?: boolean): Promise; - public hasKey(key: string): boolean; - public addKey(key: string, options: SchemaFolderAddOptions, force?: boolean): Promise; - public removeKey(key: string, force?: boolean): Promise; - public force(action: 'add' | 'edit' | 'delete', key: string, piece: SchemaFolder | SchemaPiece): Promise; - public getList(msg: KlasaMessage): string; + public add(key: string, options: SchemaFolderAddOptions | { [k: string]: SchemaFolderAddOptions }, force?: boolean): Promise; + public has(key: string): boolean; + public remove(key: string, force?: boolean): Promise; + public force(action: 'add' | 'delete', key: string, piece: SchemaFolder | SchemaPiece): Promise; public getDefaults(data?: object): object; public getSQL(array?: string[]): string[]; - public getKeys(array?: string[]): string[]; - public getValues(array?: SchemaPiece[]): SchemaPiece[]; - public resolveString(): string; - private _addKey(key: string, options: SchemaFolderAddOptions, type: typeof Schema | typeof SchemaFolder): void; - private _removeKey(key: string): void; + private _add(key: string, options: SchemaFolderAddOptions, type: typeof Schema | typeof SchemaFolder): void; + private _remove(key: string): void; + private _shardSyncSchema(piece: SchemaFolder | SchemaPiece, action: 'add' | 'delete' | 'update', force: boolean): Promise; private _init(options: object): true; + public entries(recursive?: boolean): Iterator<[string, SchemaFolder | SchemaPiece]>; + public values(recursive?: boolean): Iterator; + public keys(recursive?: boolean): Iterator; + public [Symbol.iterator](): Iterator<[string, SchemaFolder | SchemaPiece]>; + public toJSON(): any; public toString(): string; } @@ -672,9 +684,9 @@ declare module 'klasa' { public setValidator(fn: Function): this; public parse(value: any, guild: KlasaGuild): Promise; - public resolveString(msg: KlasaMessage): string; - public modify(options: SchemaPieceModifyOptions): Promise; + public modify(options: SchemaPieceEditOptions): Promise; + private _generateDefault(): Array | false | null; private _schemaCheckType(type: string): void; private _schemaCheckArray(array: boolean): void; private _schemaCheckDefault(options: SchemaFolderAddOptions): void; @@ -1038,11 +1050,6 @@ declare module 'klasa' { CLIENT: KlasaConstantsClient, CONSOLE: KlasaConsoleConfig }; - GATEWAY_RESOLVERS: { - GUILDS: (guildResolvable: string | KlasaGuild) => KlasaGuild, - USERS: (userResolvable: string | KlasaUser) => KlasaUser, - CLIENT_STORAGE: (clientResolvable: string | KlasaClient) => ClientUser - }; CRON: { allowedNum: number[][]; partRegex: RegExp; @@ -1282,6 +1289,7 @@ declare module 'klasa' { public static getDeepTypeName(input: any): string; public static getDeepTypeProxy(input: Proxy): string; public static getDeepTypeSetOrMap(input: Array | Set | WeakSet, basic?: string): string; + public static getIdentifier(value: any): string; public static getTypeName(input: any): string; public static isClass(input: Function): boolean; public static isFunction(input: Function): boolean; @@ -1289,6 +1297,7 @@ declare module 'klasa' { public static isObject(input: object): boolean; public static isThenable(input: Promise): boolean; public static makeObject(path: string, value: any): object; + public static arraysEqual(arr1: any[], arr2: any[], clone?: boolean): boolean; public static mergeDefault(def: object, given?: object): object; public static mergeObjects(objTarget: object, objSource: object): object; public static regExpEsc(str: string): string; @@ -1408,13 +1417,25 @@ declare module 'klasa' { }; // Settings + export type GatewayOptions = { + provider: Provider; + nice?: boolean; + }; + + export type GatewayJSON = { + type: string; + options: GatewayOptions; + schema: SchemaFolderJSON; + }; + export type GatewayGetPathOptions = { avoidUnconfigurable?: boolean; piece?: boolean; + errors?: boolean; }; export type GatewayGetPathResult = { - path: SchemaPiece; + piece: SchemaPiece; route: string[]; }; @@ -1422,6 +1443,7 @@ declare module 'klasa' { | KlasaMessage | KlasaTextChannel | KlasaVoiceChannel + | KlasaCategoryChannel | GuildMember | Role; @@ -1432,44 +1454,13 @@ declare module 'klasa' { }; export type ConfigurationUpdateResult = { - path: SchemaPiece; - value: any; - }; - - export type ConfigurationUpdateObjectResult = { - updated: ConfigurationUpdateObjectList; - errors: Error[]; - }; - - export type ConfigurationUpdateObjectList = { - keys: string[]; - values: any[]; - }; - - type ConfigurationParseResult = { - array?: any[]; - entryID: string; - parsed: any; - parsedID: string | number | object; - settings: Configuration; - } & ConfigurationPathResult; - - type ConfigurationUpdateManyList = { errors: Error[]; - keys: string[]; - promises: Array>; - values: any[]; + updated: ConfigurationUpdateResultEntry[]; }; - export type ConfigurationUpdateManyResult = { - errors: Error[]; - updated: ConfigurationUpdateObjectList; - }; - - export type ConfigUpdateEntryMany = { - type: 'MANY'; - keys: string[]; - values: any[]; + export type ConfigurationUpdateResultEntry = { + data: [string, any]; + piece: SchemaPiece; }; export type GatewayGuildResolvable = KlasaGuild @@ -1504,7 +1495,7 @@ declare module 'klasa' { configurable?: boolean; }; - export type SchemaPieceModifyOptions = { + export type SchemaPieceEditOptions = { default?: any; min?: number; max?: number; @@ -1522,6 +1513,21 @@ declare module 'klasa' { configurable: boolean; }; + export type SchemaFolderJSON = { + type: 'Folder'; + [k: string]: SchemaPieceJSON | SchemaFolderJSON | string; + }; + + export type GatewayDriverJSON = { + types: string[]; + keys: string[]; + ready: boolean; + guilds: GatewayJSON; + users: GatewayJSON; + clientStorage: GatewayJSON; + [k: string]: GatewayJSON | any; + }; + // Structures export type PieceOptions = { name?: string,