Upgrading from djs v12 (commando) to djs v14 (and sapphire framework) - Unsure on how to Parse Types

Hello! I have recently started upgrading an outdated open source project, and got stuck for a couple days on a specific aspect (almost done with everything else though). Context: In the configuration command, it takes two arguments, first being the key (or what setting to change), and second being the new value. Problem: The old project had a parseType function (under Utils.ts), which uses Commando's ArgumentCollector and ArgumentInfo - these do not exist in vanilla dj.s v14 or Sapphire Framework. This parseType function is extremely crucial, as it checks if the VALUE input is valid for the config (ex. councilorRole would take a role mention or role ID, and the parseType would return the Role object IF VALID somehow?). This function also takes a custom serializer that tells it the Object type and how to serialize/what function to run (ex. councilorRole key has type "role" for value, and runs the getId function). I am unsure on how to move forward with this, I have a working serializer (at least I believe) that hasn't been changed, but I am unsure on how to return the object. Depending on the key, the value could be anything from a boolean, role, channel, or JSON. The original project & files in question: command (line for parseType) https://github.com/evaera/Votum/blob/581f213f84786296f35d418f52178226e8cd2561/src/commands/votum/ConfigCommand.ts#L112 parseType: https://github.com/evaera/Votum/blob/581f213f84786296f35d418f52178226e8cd2561/src/Util.ts#L71 serializer that is fed: https://github.com/evaera/Votum/blob/581f213f84786296f35d418f52178226e8cd2561/src/commands/votum/ConfigCommand.ts#L88C4-L88C4 the actual serializer that is imported: https://github.com/evaera/Votum/blob/581f213f84786296f35d418f52178226e8cd2561/src/CouncilData.ts#L83 Will post my ConfigCommand.ts file below! - Utils.ts and CouncilData.ts (which has the serializer) are the exact same, but I need a new parseType() function that doesn't depend on Commando (so Utils.ts needs to be altered).
GitHub
Votum/src/commands/votum/ConfigCommand.ts at 581f213f84786296f35d41...
A Discord bot for managing small party voting systems - evaera/Votum
GitHub
Votum/src/CouncilData.ts at 581f213f84786296f35d418f52178226e8cd256...
A Discord bot for managing small party voting systems - evaera/Votum
GitHub
Votum/src/Util.ts at 581f213f84786296f35d418f52178226e8cd2561 · eva...
A Discord bot for managing small party voting systems - evaera/Votum
Solution:
My solution: ```ts const valueType = (serializer.type as any) try { args.restore()...
Jump to solution
6 Replies
Spikers
Spikers8mo ago
My Sapphire.js styled ConfigCommand:
import { Message } from "discord.js"
import { Args, PieceContext } from "@sapphire/framework"
import { ConfigurableCouncilData, ConfigurableCouncilDataSerializers, OptionalCouncilData } from "../../CouncilData"
import { getDefaultValue, getProps, parseType, response, ResponseType } from "../../Util"
import ICommand from "../ICommand"

const makeDisplay = (displayer?: (value: any) => string) => (value: any) => {
if (value === undefined || value === null) {
return "None"
} else if (displayer) {
return displayer(value)
} else {
return value.toString()
}
}

export default class ConfigCommand extends ICommand {
constructor(client: PieceContext) {
super(client, {
name: "config",
aliases: ["votumconfig", "cfg", "vconfig", "vcfg", "councilconfig"],
description: "Designates a specific role for councilors.",
adminOnly: true,
})
}

async execute(msg: Message, args: Args): Promise<Message | Message[]> {
const argsKey = args.next();
const argsValue = args.next();
if (argsKey == null || argsKey.length === 0) {
return msg.reply({
embeds: [response(
ResponseType.Neutral,
`Available configuration points are:\n${Object.keys(
getProps(ConfigurableCouncilData),
)
.map((n) => `~${n}~`)
.join(",\n ")}.`,
).embed],
})
}

const key = argsKey.replace(/\.([a-z0-9])/g, (_, l) =>
l.toUpperCase(),
) as keyof ConfigurableCouncilData

if (!(key in getProps(ConfigurableCouncilData))) {
return msg.reply({
embeds: [response(
ResponseType.Bad,
`:x: \`${key}\` is not a valid configuration point.`,
).embed],
})
}

const serializer = ConfigurableCouncilDataSerializers[key]
const display = makeDisplay(serializer.display)

if (argsValue == null || argsValue.length === 0) {
return msg.reply({
embeds: [response(
ResponseType.Neutral,
`Configuration point ${argsKey} is currently set to ~${display(
this.council.getConfig(key),
)}~.`,
).embed],
})
}

if (argsValue === "$remove" && key in getProps(OptionalCouncilData)) {
this.council.setConfig(key, getDefaultValue(key, OptionalCouncilData))
return msg.reply({
embeds: [response(
ResponseType.Neutral,
`Set configuration point ~${key}~ back to default state.`,
).embed],
})
}

// Line & function in question
const value = await parseType(this.client, msg, argsValue, serializer)
console.log(value)

if (value !== null) {
const serializedValue = serializer.serialize(value)

this.council.setConfig(key, serializedValue)

return msg.reply({
embeds: [response(
ResponseType.Good,
`Set configuration point ~${key}~ to ${display(value)}`,
).embed],
})
} else {
return msg.reply({
embeds: [response(
ResponseType.Bad,
`~${argsValue}~ is not a valid **${serializer.type}**`,
).embed],
})
}
}
}
import { Message } from "discord.js"
import { Args, PieceContext } from "@sapphire/framework"
import { ConfigurableCouncilData, ConfigurableCouncilDataSerializers, OptionalCouncilData } from "../../CouncilData"
import { getDefaultValue, getProps, parseType, response, ResponseType } from "../../Util"
import ICommand from "../ICommand"

const makeDisplay = (displayer?: (value: any) => string) => (value: any) => {
if (value === undefined || value === null) {
return "None"
} else if (displayer) {
return displayer(value)
} else {
return value.toString()
}
}

export default class ConfigCommand extends ICommand {
constructor(client: PieceContext) {
super(client, {
name: "config",
aliases: ["votumconfig", "cfg", "vconfig", "vcfg", "councilconfig"],
description: "Designates a specific role for councilors.",
adminOnly: true,
})
}

async execute(msg: Message, args: Args): Promise<Message | Message[]> {
const argsKey = args.next();
const argsValue = args.next();
if (argsKey == null || argsKey.length === 0) {
return msg.reply({
embeds: [response(
ResponseType.Neutral,
`Available configuration points are:\n${Object.keys(
getProps(ConfigurableCouncilData),
)
.map((n) => `~${n}~`)
.join(",\n ")}.`,
).embed],
})
}

const key = argsKey.replace(/\.([a-z0-9])/g, (_, l) =>
l.toUpperCase(),
) as keyof ConfigurableCouncilData

if (!(key in getProps(ConfigurableCouncilData))) {
return msg.reply({
embeds: [response(
ResponseType.Bad,
`:x: \`${key}\` is not a valid configuration point.`,
).embed],
})
}

const serializer = ConfigurableCouncilDataSerializers[key]
const display = makeDisplay(serializer.display)

if (argsValue == null || argsValue.length === 0) {
return msg.reply({
embeds: [response(
ResponseType.Neutral,
`Configuration point ${argsKey} is currently set to ~${display(
this.council.getConfig(key),
)}~.`,
).embed],
})
}

if (argsValue === "$remove" && key in getProps(OptionalCouncilData)) {
this.council.setConfig(key, getDefaultValue(key, OptionalCouncilData))
return msg.reply({
embeds: [response(
ResponseType.Neutral,
`Set configuration point ~${key}~ back to default state.`,
).embed],
})
}

// Line & function in question
const value = await parseType(this.client, msg, argsValue, serializer)
console.log(value)

if (value !== null) {
const serializedValue = serializer.serialize(value)

this.council.setConfig(key, serializedValue)

return msg.reply({
embeds: [response(
ResponseType.Good,
`Set configuration point ~${key}~ to ${display(value)}`,
).embed],
})
} else {
return msg.reply({
embeds: [response(
ResponseType.Bad,
`~${argsValue}~ is not a valid **${serializer.type}**`,
).embed],
})
}
}
}
I don't know how I only thought of this now (I've been trying to figure it out for the past two days), but would it be possible to have a switch case, manually going through all of the configurable options / keys, and depending on what it needs have it set const argsValue = args.next(); to argsValue = args.pick('type');? (ex: if argsKey = councilorRole it would argsValue = args.pick('role');)
Favna
Favna8mo ago
lots of text and admittedly havent read in detail yet but reading this part:
This parseType function is extremely crucial, as it checks if the VALUE input is valid for the config (ex. councilorRole would take a role mention or role ID, and the parseType would return the Role object IF VALID somehow?). This function also takes a custom serializer that tells it the Object type and how to serialize/what function to run (ex. councilorRole key has type "role" for value, and runs the getId function).
Sounds like you just want to make your own Argument: https://www.sapphirejs.dev/docs/Guide/arguments/creating-your-own-arguments You have access to previously parsed values (through context.message.content) as well so that will let you hone into whatever you need and it goes through the regular sapphire args flow that way.
Sapphire Framework
Creating your own arguments | Sapphire
Creating an argument can be done by extending the Argument class which can be imported from the framework:
Favna
Favna8mo ago
Also maybe you can take some inspiration from @Skyra's conf command: https://github.com/skyra-project/skyra/blob/refactor/v14/src/commands/Admin/conf.ts (Note that this is a WIP branch, but I believe @kyra 🩵🩷🤍🩷🩵 finished conf already. At least she did show me recently that build errors are resolved in the code)
kyra
kyra8mo ago
(Although I'm soon replacing that entire command with slashies in the future, using something similar to what Iriss does)
Spikers
Spikers8mo ago
Thank you for your input and guidance, I now have it working!!
Solution
Spikers
Spikers8mo ago
My solution:
const valueType = (serializer.type as any)
try {
args.restore()
value = await args.rest(valueType)
} catch (e) {
value = null
}
const valueType = (serializer.type as any)
try {
args.restore()
value = await args.rest(valueType)
} catch (e) {
value = null
}
And I did end up having to create three custom arguments (I was referencing the guidelines prior to opening this, and couldn't figure out how to create one or at least export it, but I then realized it was some caching issue which was resolved with me deleting all my compiled files and recompiling). Thank you again!