1. 1 : const SQL = require("../sql");
  2. 2 :
  3. 3 : /**
  4. 4 : * The gateway for this settings instance. The gateway handles all the creation and setting of non-default entries, along with saving.
  5. 5 : */
  6. 6 :
  7. 7 : class Gateway {
  8. 8 :
  9. 9 : /**
  10. 10 : * Constructs our instance of Gateway
  11. 11 : * @param {any} settings The settings that created this gateway.
  12. 12 : * @param {any} validateFunction The validation function used to validate user input.
  13. 13 : */
  14. 14 : constructor(settings, validateFunction) {
  15. 15 : /**
  16. 16 : * The Settings class that this gateway is a part of.
  17. 17 : * @name Gateway.settings
  18. 18 : * @type {Settings}
  19. 19 : * @readonly
  20. 20 : */
  21. 21 : Object.defineProperty(this, "settings", { value: settings });
  22. 22 :
  23. 23 : /**
  24. 24 : * The provider engine that will handle saving and getting all data for this instance.
  25. 25 : * @type {string}
  26. 26 : */
  27. 27 : this.engine = this.client.config.provider.engine;
  28. 28 :
  29. 29 : if (!this.provider) throw `This provider(${this.engine}) does not exist in your system.`;
  30. 30 :
  31. 31 : /**
  32. 32 : * If the provider is SQL, this property will ensure data is serialized and deserialized.
  33. 33 : * @type {string}
  34. 34 : */
  35. 35 : this.sql = this.provider.conf.sql ? new SQL(this.client, this) : null;
  36. 36 :
  37. 37 : /**
  38. 38 : * The function validator for this gateway.
  39. 39 : * @type {function}
  40. 40 : */
  41. 41 : this.validate = validateFunction;
  42. 42 : }
  43. 43 :
  44. 44 : /**
  45. 45 : * Initializes the gateway, creating tables, ensuring the schema exists, and caching values.
  46. 46 : * @param {Schema} schema The Schema object, validated from settings.
  47. 47 : * @returns {void}
  48. 48 : */
  49. 49 : async init(schema) {
  50. 50 : if (!(await this.provider.hasTable(this.type))) await this.provider.createTable(this.type, this.sql ? this.sql.buildSQLSchema(schema) : undefined);
  51. 51 : const data = await this.provider.getAll(this.type);
  52. 52 : if (this.sql) {
  53. 53 : this.sql.initDeserialize();
  54. 54 : for (let i = 0; i < data.length; i++) this.sql.deserializer(data[i]);
  55. 55 : }
  56. 56 : for (const key of data) this.cache.set(this.type, key.id, key); // eslint-disable-line
  57. 57 : }
  58. 58 :
  59. 59 : /**
  60. 60 : * Creates a new entry in the cache.
  61. 61 : * @param {Object|string} input An object containing a id property, like discord.js objects, or a string.
  62. 62 : */
  63. 63 : async create(input) {
  64. 64 : const target = await this.validate(input).then(output => (output.id || output));
  65. 65 : await this.provider.create(this.type, target, this.schema.defaults);
  66. 66 : this.cache.set(this.type, target, this.schema.defaults);
  67. 67 : }
  68. 68 :
  69. 69 : /**
  70. 70 : * Removes an entry from the cache.
  71. 71 : * @param {Object|string} input An object containing a id property, like discord.js objects, or a string.
  72. 72 : */
  73. 73 : async destroy(input) {
  74. 74 : const target = await this.validate(input).then(output => (output.id || output));
  75. 75 : await this.provider.delete(this.type, target);
  76. 76 : this.cache.delete(this.type, target);
  77. 77 : }
  78. 78 :
  79. 79 : /**
  80. 80 : * Gets an entry from the cache
  81. 81 : * @param {string} input The key you are you looking for.
  82. 82 : * @returns {Schema}
  83. 83 : */
  84. 84 : get(input) {
  85. 85 : return input !== "default" ? this.cache.get(this.type, input) || this.schema.defaults : this.schema.defaults;
  86. 86 : }
  87. 87 :
  88. 88 : /**
  89. 89 : * Sync either all entries from the provider, or a single one.
  90. 90 : * @param {Object|string} [input=null] An object containing a id property, like discord.js objects, or a string.
  91. 91 : * @returns {void}
  92. 92 : */
  93. 93 : async sync(input = null) {
  94. 94 : if (!input) {
  95. 95 : const data = await this.provider.getAll(this.type);
  96. 96 : if (this.sql) for (let i = 0; i < data.length; i++) this.sql.deserializer(data[i]);
  97. 97 : for (const key of data) this.cache.set(this.type, key.id, key); // eslint-disable-line
  98. 98 : return;
  99. 99 : }
  100. 100 : const target = await this.validate(input).then(output => (output.id || output));
  101. 101 : const data = await this.provider.get(this.type, target);
  102. 102 : if (this.sql) this.sql.deserializer(data);
  103. 103 : await this.cache.set(this.type, target, data);
  104. 104 : }
  105. 105 :
  106. 106 : /**
  107. 107 : * Reset a key's value to default from a entry.
  108. 108 : * @param {Object|string} input An object containing a id property, like Discord.js objects, or a string.
  109. 109 : * @param {string} key The key to reset.
  110. 110 : * @returns {any}
  111. 111 : */
  112. 112 : async reset(input, key) {
  113. 113 : const target = await this.validate(input).then(output => (output.id || output));
  114. 114 : if (!(key in this.schema)) throw `The key ${key} does not exist in the current data schema.`;
  115. 115 : const defaultKey = this.schema[key].default;
  116. 116 : await this.provider.update(this.type, target, { [key]: defaultKey });
  117. 117 : this.sync(target);
  118. 118 : return defaultKey;
  119. 119 : }
  120. 120 :
  121. 121 : /**
  122. 122 : * Updates an entry.
  123. 123 : * @param {Object|string} input An object or string that can be parsed by this instance's resolver.
  124. 124 : * @param {Object} object An object with pairs of key/value to update.
  125. 125 : * @param {Object|string} [guild=null] A Guild resolvable, useful for when the instance of SG doesn't aim for Guild settings.
  126. 126 : * @returns {Object}
  127. 127 : */
  128. 128 : async update(input, object, guild = null) {
  129. 129 : const target = await this.validate(input).then(output => output.id || output);
  130. 130 : guild = await this.resolver.guild(guild || target);
  131. 131 :
  132. 132 : const resolved = await Promise.all(Object.entries(object).map(async ([key, value]) => {
  133. 133 : if (!(key in this.schema)) throw `The key ${key} does not exist in the current data schema.`;
  134. 134 : return this.resolver[this.schema[key].type.toLowerCase()](value, guild, this.schema[key])
  135. 135 : .then(res => ({ [key]: res.id || res }));
  136. 136 : }));
  137. 137 :
  138. 138 : const result = Object.assign({}, ...resolved);
  139. 139 :
  140. 140 : await this.ensureCreate(target);
  141. 141 : await this.provider.update(this.type, target, result);
  142. 142 : await this.sync(target);
  143. 143 : return result;
  144. 144 : }
  145. 145 :
  146. 146 : /**
  147. 147 : * Creates the settings if it did not exist previously.
  148. 148 : * @param {Object|string} target An object or string that can be parsed by this instance's resolver.
  149. 149 : * @returns {true}
  150. 150 : */
  151. 151 : async ensureCreate(target) {
  152. 152 : if (typeof target !== "string") throw `Expected input type string, got ${typeof target}`;
  153. 153 : let exists = this.cache.has(this.type, target);
  154. 154 : if (exists instanceof Promise) exists = await exists;
  155. 155 : if (exists === false) return this.create(target);
  156. 156 : return true;
  157. 157 : }
  158. 158 :
  159. 159 : /**
  160. 160 : * Update an array from the a Guild's configuration.
  161. 161 : * @param {Object|string} input An object containing a id property, like discord.js objects, or a string.
  162. 162 : * @param {string} type Either 'add' or 'remove'.
  163. 163 : * @param {string} key The key from the Schema.
  164. 164 : * @param {any} data The value to be added or removed.
  165. 165 : * @param {Object|string} [guild=null] The guild for this setting, useful for when the settings aren't aimed for guilds
  166. 166 : * @returns {boolean}
  167. 167 : */
  168. 168 : async updateArray(input, type, key, data, guild = null) {
  169. 169 : if (!["add", "remove"].includes(type)) throw "The type parameter must be either add or remove.";
  170. 170 : if (!(key in this.schema)) throw `The key ${key} does not exist in the current data schema.`;
  171. 171 : if (!this.schema[key].array) throw `The key ${key} is not an Array.`;
  172. 172 : if (data === undefined) throw "You must specify the value to add or filter.";
  173. 173 : const target = await this.validate(input).then(output => (output.id || output));
  174. 174 : guild = await this.resolver.guild(guild || target);
  175. 175 : let result = await this.resolver[this.schema[key].type.toLowerCase()](data, guild, this.schema[key]);
  176. 176 : if (result.id) result = result.id;
  177. 177 : let cache = this.cache.get(this.type, target);
  178. 178 : if (cache instanceof Promise) cache = await cache;
  179. 179 : if (type === "add") {
  180. 180 : if (cache[key].includes(result)) throw `The value ${data} for the key ${key} already exists.`;
  181. 181 : cache[key].push(result);
  182. 182 : await this.provider.update(this.type, target, { [key]: cache[key] });
  183. 183 : await this.sync(target);
  184. 184 : return result;
  185. 185 : }
  186. 186 : if (!cache[key].includes(result)) throw `The value ${data} for the key ${key} does not exist.`;
  187. 187 : cache[key] = cache[key].filter(v => v !== result);
  188. 188 :
  189. 189 : await this.ensureCreate(target);
  190. 190 : await this.provider.update(this.type, target, { [key]: cache[key] });
  191. 191 : await this.sync(target);
  192. 192 : return true;
  193. 193 : }
  194. 194 :
  195. 195 : /**
  196. 196 : * The client this SettingGateway was created with.
  197. 197 : * @type {KomadaClient}
  198. 198 : * @readonly
  199. 199 : */
  200. 200 : get client() {
  201. 201 : return this.settings.client;
  202. 202 : }
  203. 203 :
  204. 204 : /**
  205. 205 : * The resolver instance this SettingGateway uses to parse the data.
  206. 206 : * @type {Resolver}
  207. 207 : * @readonly
  208. 208 : */
  209. 209 : get resolver() {
  210. 210 : return this.settings.resolver;
  211. 211 : }
  212. 212 :
  213. 213 : /**
  214. 214 : * The provider this SettingGateway instance uses for the persistent data operations.
  215. 215 : * @type {Provider}
  216. 216 : * @readonly
  217. 217 : */
  218. 218 : get provider() {
  219. 219 : return this.client.providers.get(this.engine);
  220. 220 : }
  221. 221 :
  222. 222 : /**
  223. 223 : * The schema this gateway instance is handling.
  224. 224 : * @type {Schema}
  225. 225 : * @readonly
  226. 226 : */
  227. 227 : get schema() {
  228. 228 : return this.settings.schema;
  229. 229 : }
  230. 230 :
  231. 231 : /**
  232. 232 : * The cache created with this instance
  233. 233 : * @type {Cache}
  234. 234 : * @readonly
  235. 235 : */
  236. 236 :
  237. 237 : get cache() {
  238. 238 : return this.settings.cache;
  239. 239 : }
  240. 240 :
  241. 241 : /**
  242. 242 : * The type of settings (or name).
  243. 243 : * @type {string}
  244. 244 : * @readonly
  245. 245 : */
  246. 246 : get type() {
  247. 247 : return this.settings.type;
  248. 248 : }
  249. 249 :
  250. 250 : }
  251. 251 :
  252. 252 : module.exports = Gateway;