* Build a `list / add / remove / clear` subcommand group around an * array-typed settings field (`allowFrom` or `groupAllowFrom`). All write * paths read existing settings first and merge — passing only a partial * `settings` object to the TRPC `update` would replace the whole JSONB * column and
(bot: Command, opts: AllowlistGroupOptions)
| 147 | * column and silently drop unrelated fields. |
| 148 | */ |
| 149 | function registerAllowlistCommand(bot: Command, opts: AllowlistGroupOptions) { |
| 150 | const group = bot.command(opts.name).description(opts.description); |
| 151 | |
| 152 | // Read the current entries off a freshly-fetched bot row. |
| 153 | const readEntries = (bot: any): AllowEntry[] => |
| 154 | normalizeAllowList((bot.settings as Record<string, unknown> | null)?.[opts.fieldKey]); |
| 155 | |
| 156 | // Build the next settings payload from existing settings + the new entries. |
| 157 | const buildPayload = (bot: any, nextEntries: AllowEntry[]) => ({ |
| 158 | id: bot.id, |
| 159 | settings: { |
| 160 | ...(bot.settings as Record<string, unknown>), |
| 161 | [opts.fieldKey]: nextEntries, |
| 162 | }, |
| 163 | }); |
| 164 | |
| 165 | group |
| 166 | .command('list <botId>') |
| 167 | .description(`List ${opts.fieldKey} entries`) |
| 168 | .option('--json', 'Output JSON') |
| 169 | .action(async (botId: string, options: { json?: boolean }) => { |
| 170 | const client = await getTrpcClient(); |
| 171 | const b = await findBot(client, botId); |
| 172 | const entries = readEntries(b); |
| 173 | |
| 174 | if (options.json) { |
| 175 | outputJson(entries); |
| 176 | return; |
| 177 | } |
| 178 | |
| 179 | if (entries.length === 0) { |
| 180 | console.log(`${pc.dim(`No ${opts.fieldKey} entries.`)}`); |
| 181 | return; |
| 182 | } |
| 183 | |
| 184 | printTable( |
| 185 | entries.map((e) => [e.id, e.name ?? pc.dim('-')]), |
| 186 | ['ID', 'NAME'], |
| 187 | ); |
| 188 | }); |
| 189 | |
| 190 | group |
| 191 | .command('add <botId> <id>') |
| 192 | .description(`Add a ${opts.idLabel} to ${opts.fieldKey}`) |
| 193 | .option('--name <name>', 'Optional human-friendly label so you can recognise the entry later') |
| 194 | .action(async (botId: string, id: string, options: { name?: string }) => { |
| 195 | const trimmedId = id.trim(); |
| 196 | if (!trimmedId) { |
| 197 | log.error('ID cannot be empty.'); |
| 198 | process.exit(1); |
| 199 | return; |
| 200 | } |
| 201 | |
| 202 | const client = await getTrpcClient(); |
| 203 | const b = await findBot(client, botId); |
| 204 | const entries = readEntries(b); |
| 205 | |
| 206 | if (entries.some((e) => e.id === trimmedId)) { |
no test coverage detected