@azurioh/discord-kernel 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -1
- package/dist/authz/require-level-guard.d.ts +1 -1
- package/dist/authz/require-level-guard.d.ts.map +1 -1
- package/dist/color.d.ts +42 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +130 -0
- package/dist/color.js.map +1 -0
- package/dist/discord/command/all-required.d.ts +8 -0
- package/dist/discord/command/all-required.d.ts.map +1 -0
- package/dist/discord/command/all-required.js +12 -0
- package/dist/discord/command/all-required.js.map +1 -0
- package/dist/discord/command/autocomplete-dispatcher.d.ts.map +1 -1
- package/dist/discord/command/autocomplete-dispatcher.js +2 -1
- package/dist/discord/command/autocomplete-dispatcher.js.map +1 -1
- package/dist/discord/command/autocomplete-limits.d.ts +3 -0
- package/dist/discord/command/autocomplete-limits.d.ts.map +1 -0
- package/dist/discord/command/autocomplete-limits.js +6 -0
- package/dist/discord/command/autocomplete-limits.js.map +1 -0
- package/dist/discord/command/autocomplete.d.ts +6 -1
- package/dist/discord/command/autocomplete.d.ts.map +1 -1
- package/dist/discord/command/autocomplete.js +6 -6
- package/dist/discord/command/autocomplete.js.map +1 -1
- package/dist/discord/command/command-responders.d.ts +26 -0
- package/dist/discord/command/command-responders.d.ts.map +1 -0
- package/dist/discord/command/command-responders.js +16 -0
- package/dist/discord/command/command-responders.js.map +1 -0
- package/dist/discord/command/context.d.ts +8 -17
- package/dist/discord/command/context.d.ts.map +1 -1
- package/dist/discord/command/context.js +8 -37
- package/dist/discord/command/context.js.map +1 -1
- package/dist/discord/command/cooldown-guard.d.ts +1 -1
- package/dist/discord/command/cooldown-guard.d.ts.map +1 -1
- package/dist/discord/command/create-command.d.ts +1 -8
- package/dist/discord/command/create-command.d.ts.map +1 -1
- package/dist/discord/command/create-command.js +17 -65
- package/dist/discord/command/create-command.js.map +1 -1
- package/dist/discord/command/create-context-menu-command.d.ts +3 -9
- package/dist/discord/command/create-context-menu-command.d.ts.map +1 -1
- package/dist/discord/command/create-context-menu-command.js +14 -65
- package/dist/discord/command/create-context-menu-command.js.map +1 -1
- package/dist/discord/command/errors.d.ts +1 -1
- package/dist/discord/command/errors.d.ts.map +1 -1
- package/dist/discord/command/errors.js +2 -2
- package/dist/discord/command/errors.js.map +1 -1
- package/dist/discord/command/guard.d.ts +15 -8
- package/dist/discord/command/guard.d.ts.map +1 -1
- package/dist/discord/command/guard.js +4 -4
- package/dist/discord/command/guard.js.map +1 -1
- package/dist/discord/command/options.d.ts +1 -10
- package/dist/discord/command/options.d.ts.map +1 -1
- package/dist/discord/command/options.js +127 -320
- package/dist/discord/command/options.js.map +1 -1
- package/dist/discord/command/passes-guard.d.ts +14 -0
- package/dist/discord/command/passes-guard.d.ts.map +1 -0
- package/dist/discord/command/passes-guard.js +27 -0
- package/dist/discord/command/passes-guard.js.map +1 -0
- package/dist/discord/command/permission-guard.d.ts +1 -1
- package/dist/discord/command/permission-guard.d.ts.map +1 -1
- package/dist/discord/command/render-command-failure.d.ts +23 -0
- package/dist/discord/command/render-command-failure.d.ts.map +1 -0
- package/dist/discord/command/render-command-failure.js +40 -0
- package/dist/discord/command/render-command-failure.js.map +1 -0
- package/dist/discord/command/route-key.d.ts +8 -0
- package/dist/discord/command/route-key.d.ts.map +1 -0
- package/dist/discord/command/route-key.js +13 -0
- package/dist/discord/command/route-key.js.map +1 -0
- package/dist/discord/command/router.d.ts +32 -7
- package/dist/discord/command/router.d.ts.map +1 -1
- package/dist/discord/command/router.js +48 -10
- package/dist/discord/command/router.js.map +1 -1
- package/dist/discord/command/send-embed.d.ts +14 -0
- package/dist/discord/command/send-embed.d.ts.map +1 -0
- package/dist/discord/command/send-embed.js +37 -0
- package/dist/discord/command/send-embed.js.map +1 -0
- package/dist/discord/command/types.d.ts +7 -1
- package/dist/discord/command/types.d.ts.map +1 -1
- package/dist/discord/components/component-access.d.ts +12 -0
- package/dist/discord/components/component-access.d.ts.map +1 -0
- package/dist/discord/components/component-access.js +21 -0
- package/dist/discord/components/component-access.js.map +1 -0
- package/dist/discord/components/component-router.d.ts +37 -15
- package/dist/discord/components/component-router.d.ts.map +1 -1
- package/dist/discord/components/component-router.js +44 -32
- package/dist/discord/components/component-router.js.map +1 -1
- package/dist/discord/components/interactive-message/index.d.ts +3 -1
- package/dist/discord/components/interactive-message/index.d.ts.map +1 -1
- package/dist/discord/components/interactive-message/index.js +5 -3
- package/dist/discord/components/interactive-message/index.js.map +1 -1
- package/dist/discord/components/interactive-message/interactive-message-collector.d.ts +3 -28
- package/dist/discord/components/interactive-message/interactive-message-collector.d.ts.map +1 -1
- package/dist/discord/components/interactive-message/interactive-message-collector.js +6 -53
- package/dist/discord/components/interactive-message/interactive-message-collector.js.map +1 -1
- package/dist/discord/components/interactive-message/message-edit-options.d.ts +18 -0
- package/dist/discord/components/interactive-message/message-edit-options.d.ts.map +1 -0
- package/dist/discord/components/interactive-message/message-edit-options.js +40 -0
- package/dist/discord/components/interactive-message/message-edit-options.js.map +1 -0
- package/dist/discord/components/interactive-message/mount-interactive-message.d.ts +2 -1
- package/dist/discord/components/interactive-message/mount-interactive-message.d.ts.map +1 -1
- package/dist/discord/components/interactive-message/mount-interactive-message.js +4 -2
- package/dist/discord/components/interactive-message/mount-interactive-message.js.map +1 -1
- package/dist/discord/components/interactive-message/state-store.d.ts +12 -0
- package/dist/discord/components/interactive-message/state-store.d.ts.map +1 -0
- package/dist/discord/components/interactive-message/state-store.js +14 -0
- package/dist/discord/components/interactive-message/state-store.js.map +1 -0
- package/dist/discord/components/paginator/mount-paginator.js +7 -5
- package/dist/discord/components/paginator/mount-paginator.js.map +1 -1
- package/dist/discord/components/paginator/paginator-view.d.ts +1 -1
- package/dist/discord/components/paginator/paginator-view.d.ts.map +1 -1
- package/dist/discord/components/persistent-paginator.d.ts +1 -1
- package/dist/discord/components/persistent-paginator.d.ts.map +1 -1
- package/dist/discord/components/persistent-paginator.js +6 -6
- package/dist/discord/components/persistent-paginator.js.map +1 -1
- package/dist/discord/components/settings-editor/from-declaration-controls.d.ts +67 -0
- package/dist/discord/components/settings-editor/from-declaration-controls.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-controls.js +245 -0
- package/dist/discord/components/settings-editor/from-declaration-controls.js.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-display.d.ts +23 -0
- package/dist/discord/components/settings-editor/from-declaration-display.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-display.js +114 -0
- package/dist/discord/components/settings-editor/from-declaration-display.js.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-layout.d.ts +33 -0
- package/dist/discord/components/settings-editor/from-declaration-layout.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-layout.js +178 -0
- package/dist/discord/components/settings-editor/from-declaration-layout.js.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-search.d.ts +27 -0
- package/dist/discord/components/settings-editor/from-declaration-search.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-search.js +58 -0
- package/dist/discord/components/settings-editor/from-declaration-search.js.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-stored.d.ts +21 -0
- package/dist/discord/components/settings-editor/from-declaration-stored.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-stored.js +37 -0
- package/dist/discord/components/settings-editor/from-declaration-stored.js.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-subject.d.ts +32 -0
- package/dist/discord/components/settings-editor/from-declaration-subject.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-subject.js +65 -0
- package/dist/discord/components/settings-editor/from-declaration-subject.js.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-values.d.ts +57 -0
- package/dist/discord/components/settings-editor/from-declaration-values.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration-values.js +187 -0
- package/dist/discord/components/settings-editor/from-declaration-values.js.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration.d.ts +39 -0
- package/dist/discord/components/settings-editor/from-declaration.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/from-declaration.js +132 -0
- package/dist/discord/components/settings-editor/from-declaration.js.map +1 -0
- package/dist/discord/components/settings-editor/index.d.ts +3 -1
- package/dist/discord/components/settings-editor/index.d.ts.map +1 -1
- package/dist/discord/components/settings-editor/index.js +6 -3
- package/dist/discord/components/settings-editor/index.js.map +1 -1
- package/dist/discord/components/settings-editor/mount-settings-editor.d.ts +16 -2
- package/dist/discord/components/settings-editor/mount-settings-editor.d.ts.map +1 -1
- package/dist/discord/components/settings-editor/mount-settings-editor.js +14 -10
- package/dist/discord/components/settings-editor/mount-settings-editor.js.map +1 -1
- package/dist/discord/components/settings-editor/navigation.d.ts +20 -0
- package/dist/discord/components/settings-editor/navigation.d.ts.map +1 -1
- package/dist/discord/components/settings-editor/navigation.js +22 -0
- package/dist/discord/components/settings-editor/navigation.js.map +1 -1
- package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts +4 -3
- package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts.map +1 -1
- package/dist/discord/components/settings-editor/settings-editor-card.view.js +60 -34
- package/dist/discord/components/settings-editor/settings-editor-card.view.js.map +1 -1
- package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts +2 -1
- package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts.map +1 -1
- package/dist/discord/components/settings-editor/settings-editor-field-modal.js +48 -72
- package/dist/discord/components/settings-editor/settings-editor-field-modal.js.map +1 -1
- package/dist/discord/components/settings-editor/settings-editor-field-values.d.ts +6 -0
- package/dist/discord/components/settings-editor/settings-editor-field-values.d.ts.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-field-values.js +13 -0
- package/dist/discord/components/settings-editor/settings-editor-field-values.js.map +1 -0
- package/dist/discord/components/settings-editor/settings-editor-fields.d.ts +6 -5
- package/dist/discord/components/settings-editor/settings-editor-fields.d.ts.map +1 -1
- package/dist/discord/components/settings-editor/settings-editor-fields.js +0 -10
- package/dist/discord/components/settings-editor/settings-editor-fields.js.map +1 -1
- package/dist/discord/components/settings-editor/settings-editor.view.d.ts +3 -2
- package/dist/discord/components/settings-editor/settings-editor.view.d.ts.map +1 -1
- package/dist/discord/components/settings-editor/settings-editor.view.js +4 -21
- package/dist/discord/components/settings-editor/settings-editor.view.js.map +1 -1
- package/dist/discord/events/event-guild-id.d.ts +10 -0
- package/dist/discord/events/event-guild-id.d.ts.map +1 -0
- package/dist/discord/events/event-guild-id.js +37 -0
- package/dist/discord/events/event-guild-id.js.map +1 -0
- package/dist/discord/events/event-router.d.ts +17 -3
- package/dist/discord/events/event-router.d.ts.map +1 -1
- package/dist/discord/events/event-router.js +33 -9
- package/dist/discord/events/event-router.js.map +1 -1
- package/dist/discord/i18n.d.ts +17 -2
- package/dist/discord/i18n.d.ts.map +1 -1
- package/dist/discord/i18n.js +43 -6
- package/dist/discord/i18n.js.map +1 -1
- package/dist/discord/interaction/interaction-locale.d.ts +14 -0
- package/dist/discord/interaction/interaction-locale.d.ts.map +1 -0
- package/dist/discord/interaction/interaction-locale.js +12 -0
- package/dist/discord/interaction/interaction-locale.js.map +1 -0
- package/dist/discord/interaction/locale-resolver.d.ts +27 -0
- package/dist/discord/interaction/locale-resolver.d.ts.map +1 -0
- package/dist/discord/interaction/locale-resolver.js +3 -0
- package/dist/discord/interaction/locale-resolver.js.map +1 -0
- package/dist/discord/interaction/modal.d.ts.map +1 -1
- package/dist/discord/interaction/modal.js +5 -40
- package/dist/discord/interaction/modal.js.map +1 -1
- package/dist/discord/interaction/reply-locale.d.ts +18 -0
- package/dist/discord/interaction/reply-locale.d.ts.map +1 -0
- package/dist/discord/interaction/reply-locale.js +25 -0
- package/dist/discord/interaction/reply-locale.js.map +1 -0
- package/dist/discord/interaction/select-bounds.d.ts +19 -0
- package/dist/discord/interaction/select-bounds.d.ts.map +1 -0
- package/dist/discord/interaction/select-bounds.js +17 -0
- package/dist/discord/interaction/select-bounds.js.map +1 -0
- package/dist/discord/interaction/select-menu.d.ts.map +1 -1
- package/dist/discord/interaction/select-menu.js +2 -9
- package/dist/discord/interaction/select-menu.js.map +1 -1
- package/dist/discord/placeholder-registry.d.ts +1 -1
- package/dist/discord/placeholder-registry.d.ts.map +1 -1
- package/dist/discord/presenter.d.ts +1 -1
- package/dist/discord/presenter.d.ts.map +1 -1
- package/dist/discord/settings/discord-guild-directory.d.ts +28 -0
- package/dist/discord/settings/discord-guild-directory.d.ts.map +1 -0
- package/dist/discord/settings/discord-guild-directory.js +171 -0
- package/dist/discord/settings/discord-guild-directory.js.map +1 -0
- package/dist/discord/settings/guild-locale-resolver.d.ts +25 -0
- package/dist/discord/settings/guild-locale-resolver.d.ts.map +1 -0
- package/dist/discord/settings/guild-locale-resolver.js +33 -0
- package/dist/discord/settings/guild-locale-resolver.js.map +1 -0
- package/dist/discord/settings/is-module-disabled.d.ts +16 -0
- package/dist/discord/settings/is-module-disabled.d.ts.map +1 -0
- package/dist/discord/settings/is-module-disabled.js +20 -0
- package/dist/discord/settings/is-module-disabled.js.map +1 -0
- package/dist/discord/settings/module-disabled-embed.d.ts +13 -0
- package/dist/discord/settings/module-disabled-embed.d.ts.map +1 -0
- package/dist/discord/settings/module-disabled-embed.js +16 -0
- package/dist/discord/settings/module-disabled-embed.js.map +1 -0
- package/dist/discord/settings/require-configured.d.ts +26 -0
- package/dist/discord/settings/require-configured.d.ts.map +1 -0
- package/dist/discord/settings/require-configured.js +62 -0
- package/dist/discord/settings/require-configured.js.map +1 -0
- package/dist/discord/ui/color-aliases.d.ts +31 -0
- package/dist/discord/ui/color-aliases.d.ts.map +1 -0
- package/dist/discord/ui/color-aliases.js +66 -0
- package/dist/discord/ui/color-aliases.js.map +1 -0
- package/dist/discord/ui/color-input.d.ts +5 -32
- package/dist/discord/ui/color-input.d.ts.map +1 -1
- package/dist/discord/ui/color-input.js +8 -157
- package/dist/discord/ui/color-input.js.map +1 -1
- package/dist/discord/ui/colors.d.ts +0 -2
- package/dist/discord/ui/colors.d.ts.map +1 -1
- package/dist/discord/ui/colors.js +4 -5
- package/dist/discord/ui/colors.js.map +1 -1
- package/dist/discord/ui/embed.d.ts.map +1 -1
- package/dist/discord/ui/embed.js +3 -8
- package/dist/discord/ui/embed.js.map +1 -1
- package/dist/discord/ui/truncate-text.d.ts +11 -0
- package/dist/discord/ui/truncate-text.d.ts.map +1 -0
- package/dist/discord/ui/truncate-text.js +19 -0
- package/dist/discord/ui/truncate-text.js.map +1 -0
- package/dist/errors/error-message.d.ts +9 -0
- package/dist/errors/error-message.d.ts.map +1 -0
- package/dist/errors/error-message.js +14 -0
- package/dist/errors/error-message.js.map +1 -0
- package/dist/i18n/business-message.d.ts +1 -1
- package/dist/i18n/business-message.d.ts.map +1 -1
- package/dist/i18n/catalog.d.ts +9 -0
- package/dist/i18n/catalog.d.ts.map +1 -1
- package/dist/i18n/catalog.js +4 -0
- package/dist/i18n/catalog.js.map +1 -1
- package/dist/i18n/locale.d.ts +2 -0
- package/dist/i18n/locale.d.ts.map +1 -1
- package/dist/i18n/locale.js +3 -1
- package/dist/i18n/locale.js.map +1 -1
- package/dist/i18n/translator.d.ts +2 -0
- package/dist/i18n/translator.d.ts.map +1 -1
- package/dist/i18n/translator.js.map +1 -1
- package/dist/module/module.d.ts +15 -1
- package/dist/module/module.d.ts.map +1 -1
- package/dist/resilience/retry.js +2 -2
- package/dist/resilience/retry.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts.map +1 -1
- package/dist/scheduler/scheduler.js +2 -4
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/settings/cache.d.ts +29 -0
- package/dist/settings/cache.d.ts.map +1 -0
- package/dist/settings/cache.js +47 -0
- package/dist/settings/cache.js.map +1 -0
- package/dist/settings/choice.d.ts +10 -0
- package/dist/settings/choice.d.ts.map +1 -0
- package/dist/settings/choice.js +3 -0
- package/dist/settings/choice.js.map +1 -0
- package/dist/settings/decode-stored.d.ts +20 -0
- package/dist/settings/decode-stored.d.ts.map +1 -0
- package/dist/settings/decode-stored.js +35 -0
- package/dist/settings/decode-stored.js.map +1 -0
- package/dist/settings/define-settings.d.ts +52 -0
- package/dist/settings/define-settings.d.ts.map +1 -0
- package/dist/settings/define-settings.js +141 -0
- package/dist/settings/define-settings.js.map +1 -0
- package/dist/settings/describe-field.d.ts +19 -0
- package/dist/settings/describe-field.d.ts.map +1 -0
- package/dist/settings/describe-field.js +131 -0
- package/dist/settings/describe-field.js.map +1 -0
- package/dist/settings/describe.d.ts +27 -0
- package/dist/settings/describe.d.ts.map +1 -0
- package/dist/settings/describe.js +62 -0
- package/dist/settings/describe.js.map +1 -0
- package/dist/settings/duration.d.ts +16 -0
- package/dist/settings/duration.d.ts.map +1 -0
- package/dist/settings/duration.js +56 -0
- package/dist/settings/duration.js.map +1 -0
- package/dist/settings/field-order.d.ts +37 -0
- package/dist/settings/field-order.d.ts.map +1 -0
- package/dist/settings/field-order.js +100 -0
- package/dist/settings/field-order.js.map +1 -0
- package/dist/settings/field-search.d.ts +10 -0
- package/dist/settings/field-search.d.ts.map +1 -0
- package/dist/settings/field-search.js +18 -0
- package/dist/settings/field-search.js.map +1 -0
- package/dist/settings/fields/builders.d.ts +41 -0
- package/dist/settings/fields/builders.d.ts.map +1 -0
- package/dist/settings/fields/builders.js +83 -0
- package/dist/settings/fields/builders.js.map +1 -0
- package/dist/settings/fields/field.d.ts +199 -0
- package/dist/settings/fields/field.d.ts.map +1 -0
- package/dist/settings/fields/field.js +33 -0
- package/dist/settings/fields/field.js.map +1 -0
- package/dist/settings/fields/zod-schema.d.ts +67 -0
- package/dist/settings/fields/zod-schema.d.ts.map +1 -0
- package/dist/settings/fields/zod-schema.js +281 -0
- package/dist/settings/fields/zod-schema.js.map +1 -0
- package/dist/settings/in-memory/in-memory-guild-directory.d.ts +24 -0
- package/dist/settings/in-memory/in-memory-guild-directory.d.ts.map +1 -0
- package/dist/settings/in-memory/in-memory-guild-directory.js +42 -0
- package/dist/settings/in-memory/in-memory-guild-directory.js.map +1 -0
- package/dist/settings/in-memory/in-memory-settings-store.d.ts +8 -0
- package/dist/settings/in-memory/in-memory-settings-store.d.ts.map +1 -0
- package/dist/settings/in-memory/in-memory-settings-store.js +43 -0
- package/dist/settings/in-memory/in-memory-settings-store.js.map +1 -0
- package/dist/settings/in-memory/in-process-notifier.d.ts +9 -0
- package/dist/settings/in-memory/in-process-notifier.d.ts.map +1 -0
- package/dist/settings/in-memory/in-process-notifier.js +32 -0
- package/dist/settings/in-memory/in-process-notifier.js.map +1 -0
- package/dist/settings/index.d.ts +22 -0
- package/dist/settings/index.d.ts.map +1 -0
- package/dist/settings/index.js +31 -0
- package/dist/settings/index.js.map +1 -0
- package/dist/settings/is-plain-object.d.ts +6 -0
- package/dist/settings/is-plain-object.d.ts.map +1 -0
- package/dist/settings/is-plain-object.js +15 -0
- package/dist/settings/is-plain-object.js.map +1 -0
- package/dist/settings/messages.d.ts +61 -0
- package/dist/settings/messages.d.ts.map +1 -0
- package/dist/settings/messages.js +155 -0
- package/dist/settings/messages.js.map +1 -0
- package/dist/settings/migrate.d.ts +35 -0
- package/dist/settings/migrate.d.ts.map +1 -0
- package/dist/settings/migrate.js +42 -0
- package/dist/settings/migrate.js.map +1 -0
- package/dist/settings/ports/guild-directory.d.ts +61 -0
- package/dist/settings/ports/guild-directory.d.ts.map +1 -0
- package/dist/settings/ports/guild-directory.js +3 -0
- package/dist/settings/ports/guild-directory.js.map +1 -0
- package/dist/settings/ports/settings-changed-notifier.d.ts +20 -0
- package/dist/settings/ports/settings-changed-notifier.d.ts.map +1 -0
- package/dist/settings/ports/settings-changed-notifier.js +3 -0
- package/dist/settings/ports/settings-changed-notifier.js.map +1 -0
- package/dist/settings/ports/settings-store.d.ts +76 -0
- package/dist/settings/ports/settings-store.d.ts.map +1 -0
- package/dist/settings/ports/settings-store.js +3 -0
- package/dist/settings/ports/settings-store.js.map +1 -0
- package/dist/settings/registry.d.ts +31 -0
- package/dist/settings/registry.d.ts.map +1 -0
- package/dist/settings/registry.js +102 -0
- package/dist/settings/registry.js.map +1 -0
- package/dist/settings/settings-declaration-error.d.ts +13 -0
- package/dist/settings/settings-declaration-error.d.ts.map +1 -0
- package/dist/settings/settings-declaration-error.js +22 -0
- package/dist/settings/settings-declaration-error.js.map +1 -0
- package/dist/settings/settings-service.d.ts +148 -0
- package/dist/settings/settings-service.d.ts.map +1 -0
- package/dist/settings/settings-service.js +279 -0
- package/dist/settings/settings-service.js.map +1 -0
- package/dist/settings/settings-validation-error.d.ts +23 -0
- package/dist/settings/settings-validation-error.d.ts.map +1 -0
- package/dist/settings/settings-validation-error.js +17 -0
- package/dist/settings/settings-validation-error.js.map +1 -0
- package/dist/settings/status.d.ts +16 -0
- package/dist/settings/status.d.ts.map +1 -0
- package/dist/settings/status.js +18 -0
- package/dist/settings/status.js.map +1 -0
- package/dist/settings/suggest.d.ts +77 -0
- package/dist/settings/suggest.d.ts.map +1 -0
- package/dist/settings/suggest.js +192 -0
- package/dist/settings/suggest.js.map +1 -0
- package/dist/settings/system/kernel-settings.d.ts +43 -0
- package/dist/settings/system/kernel-settings.d.ts.map +1 -0
- package/dist/settings/system/kernel-settings.js +53 -0
- package/dist/settings/system/kernel-settings.js.map +1 -0
- package/dist/settings/system/module-gate.d.ts +31 -0
- package/dist/settings/system/module-gate.d.ts.map +1 -0
- package/dist/settings/system/module-gate.js +34 -0
- package/dist/settings/system/module-gate.js.map +1 -0
- package/dist/settings/testing/index.d.ts +2 -0
- package/dist/settings/testing/index.d.ts.map +1 -0
- package/dist/settings/testing/index.js +6 -0
- package/dist/settings/testing/index.js.map +1 -0
- package/dist/settings/testing/settings-store-contract.d.ts +29 -0
- package/dist/settings/testing/settings-store-contract.d.ts.map +1 -0
- package/dist/settings/testing/settings-store-contract.js +192 -0
- package/dist/settings/testing/settings-store-contract.js.map +1 -0
- package/dist/settings/timed-search.d.ts +26 -0
- package/dist/settings/timed-search.d.ts.map +1 -0
- package/dist/settings/timed-search.js +48 -0
- package/dist/settings/timed-search.js.map +1 -0
- package/dist/settings/types.d.ts +37 -0
- package/dist/settings/types.d.ts.map +1 -0
- package/dist/settings/types.js +3 -0
- package/dist/settings/types.js.map +1 -0
- package/dist/settings/validate.d.ts +55 -0
- package/dist/settings/validate.d.ts.map +1 -0
- package/dist/settings/validate.js +196 -0
- package/dist/settings/validate.js.map +1 -0
- package/package.json +463 -88
- package/src/authz/require-level-guard.ts +1 -1
- package/src/color.ts +134 -0
- package/src/discord/command/all-required.ts +12 -0
- package/src/discord/command/autocomplete-dispatcher.ts +2 -1
- package/src/discord/command/autocomplete-limits.ts +2 -0
- package/src/discord/command/autocomplete.ts +8 -7
- package/src/discord/command/command-responders.ts +39 -0
- package/src/discord/command/context.ts +14 -54
- package/src/discord/command/cooldown-guard.ts +1 -1
- package/src/discord/command/create-command.ts +19 -94
- package/src/discord/command/create-context-menu-command.ts +19 -97
- package/src/discord/command/errors.ts +1 -1
- package/src/discord/command/guard.ts +24 -12
- package/src/discord/command/options.ts +202 -412
- package/src/discord/command/passes-guard.ts +37 -0
- package/src/discord/command/permission-guard.ts +1 -1
- package/src/discord/command/render-command-failure.ts +71 -0
- package/src/discord/command/route-key.ts +9 -0
- package/src/discord/command/router.ts +73 -12
- package/src/discord/command/send-embed.ts +42 -0
- package/src/discord/command/types.ts +7 -1
- package/src/discord/components/component-access.ts +20 -0
- package/src/discord/components/component-router.ts +73 -36
- package/src/discord/components/interactive-message/index.ts +5 -3
- package/src/discord/components/interactive-message/interactive-message-collector.ts +4 -68
- package/src/discord/components/interactive-message/message-edit-options.ts +47 -0
- package/src/discord/components/interactive-message/mount-interactive-message.ts +5 -3
- package/src/discord/components/interactive-message/state-store.ts +20 -0
- package/src/discord/components/paginator/mount-paginator.ts +3 -3
- package/src/discord/components/paginator/paginator-view.ts +1 -1
- package/src/discord/components/persistent-paginator.ts +8 -9
- package/src/discord/components/settings-editor/from-declaration-controls.ts +371 -0
- package/src/discord/components/settings-editor/from-declaration-display.ts +155 -0
- package/src/discord/components/settings-editor/from-declaration-layout.ts +247 -0
- package/src/discord/components/settings-editor/from-declaration-search.ts +77 -0
- package/src/discord/components/settings-editor/from-declaration-stored.ts +39 -0
- package/src/discord/components/settings-editor/from-declaration-subject.ts +110 -0
- package/src/discord/components/settings-editor/from-declaration-values.ts +240 -0
- package/src/discord/components/settings-editor/from-declaration.ts +218 -0
- package/src/discord/components/settings-editor/index.ts +5 -2
- package/src/discord/components/settings-editor/mount-settings-editor.ts +28 -10
- package/src/discord/components/settings-editor/navigation.ts +41 -0
- package/src/discord/components/settings-editor/settings-editor-card.view.ts +79 -36
- package/src/discord/components/settings-editor/settings-editor-field-modal.ts +76 -68
- package/src/discord/components/settings-editor/settings-editor-field-values.ts +11 -0
- package/src/discord/components/settings-editor/settings-editor-fields.ts +6 -11
- package/src/discord/components/settings-editor/settings-editor.view.ts +11 -24
- package/src/discord/events/event-guild-id.ts +35 -0
- package/src/discord/events/event-router.ts +46 -13
- package/src/discord/i18n.ts +47 -8
- package/src/discord/interaction/interaction-locale.ts +19 -0
- package/src/discord/interaction/locale-resolver.ts +28 -0
- package/src/discord/interaction/modal.ts +5 -40
- package/src/discord/interaction/reply-locale.ts +35 -0
- package/src/discord/interaction/select-bounds.ts +33 -0
- package/src/discord/interaction/select-menu.ts +2 -9
- package/src/discord/placeholder-registry.ts +1 -1
- package/src/discord/presenter.ts +1 -1
- package/src/discord/settings/discord-guild-directory.ts +214 -0
- package/src/discord/settings/guild-locale-resolver.ts +38 -0
- package/src/discord/settings/is-module-disabled.ts +22 -0
- package/src/discord/settings/module-disabled-embed.ts +19 -0
- package/src/discord/settings/require-configured.ts +68 -0
- package/src/discord/ui/color-aliases.ts +70 -0
- package/src/discord/ui/color-input.ts +8 -173
- package/src/discord/ui/colors.ts +2 -3
- package/src/discord/ui/embed.ts +3 -9
- package/src/discord/ui/truncate-text.ts +15 -0
- package/src/errors/error-message.ts +10 -0
- package/src/i18n/business-message.ts +1 -1
- package/src/i18n/catalog.ts +14 -0
- package/src/i18n/locale.ts +3 -0
- package/src/i18n/translator.ts +3 -0
- package/src/module/module.ts +15 -1
- package/src/resilience/retry.ts +1 -1
- package/src/scheduler/scheduler.ts +2 -5
- package/src/settings/cache.ts +70 -0
- package/src/settings/choice.ts +9 -0
- package/src/settings/decode-stored.ts +40 -0
- package/src/settings/define-settings.ts +214 -0
- package/src/settings/describe-field.ts +155 -0
- package/src/settings/describe.ts +90 -0
- package/src/settings/duration.ts +54 -0
- package/src/settings/field-order.ts +128 -0
- package/src/settings/field-search.ts +16 -0
- package/src/settings/fields/builders.ts +161 -0
- package/src/settings/fields/field.ts +205 -0
- package/src/settings/fields/zod-schema.ts +300 -0
- package/src/settings/in-memory/in-memory-guild-directory.ts +69 -0
- package/src/settings/in-memory/in-memory-settings-store.ts +46 -0
- package/src/settings/in-memory/in-process-notifier.ts +40 -0
- package/src/settings/index.ts +72 -0
- package/src/settings/is-plain-object.ts +11 -0
- package/src/settings/messages.ts +161 -0
- package/src/settings/migrate.ts +58 -0
- package/src/settings/ports/guild-directory.ts +48 -0
- package/src/settings/ports/settings-changed-notifier.ts +20 -0
- package/src/settings/ports/settings-store.ts +74 -0
- package/src/settings/registry.ts +137 -0
- package/src/settings/settings-declaration-error.ts +16 -0
- package/src/settings/settings-service.ts +535 -0
- package/src/settings/settings-validation-error.ts +41 -0
- package/src/settings/status.ts +25 -0
- package/src/settings/suggest.ts +267 -0
- package/src/settings/system/kernel-settings.ts +74 -0
- package/src/settings/system/module-gate.ts +49 -0
- package/src/settings/testing/index.ts +7 -0
- package/src/settings/testing/settings-store-contract.ts +278 -0
- package/src/settings/timed-search.ts +56 -0
- package/src/settings/types.ts +42 -0
- package/src/settings/validate.ts +298 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import type { Clock } from "@/clock";
|
|
3
|
+
import { ConflictError, ValidationError } from "@/errors/business-error";
|
|
4
|
+
import { errorMessage } from "@/errors/error-message";
|
|
5
|
+
import type { Translator } from "@/i18n/translator";
|
|
6
|
+
import type { Logger } from "@/logger";
|
|
7
|
+
import { createSettingsCache } from "@/settings/cache";
|
|
8
|
+
import type { Choice } from "@/settings/choice";
|
|
9
|
+
import { decodeStored } from "@/settings/decode-stored";
|
|
10
|
+
import type { SettingsDeclaration } from "@/settings/define-settings";
|
|
11
|
+
import { describeSettings, type SettingsSchema } from "@/settings/describe";
|
|
12
|
+
import type { AnyField } from "@/settings/fields/field";
|
|
13
|
+
import { SETTINGS_ISSUE_MESSAGES } from "@/settings/messages";
|
|
14
|
+
import { migrateStored } from "@/settings/migrate";
|
|
15
|
+
import type { GuildDirectory } from "@/settings/ports/guild-directory";
|
|
16
|
+
import type { SettingsChangedNotifier } from "@/settings/ports/settings-changed-notifier";
|
|
17
|
+
import type { SettingsStore, StoredSettings } from "@/settings/ports/settings-store";
|
|
18
|
+
import type { SettingsRegistry } from "@/settings/registry";
|
|
19
|
+
import { type SettingsIssue, SettingsValidationError } from "@/settings/settings-validation-error";
|
|
20
|
+
import { type SettingsStatus, settingsStatus } from "@/settings/status";
|
|
21
|
+
import {
|
|
22
|
+
type SuggestionPorts,
|
|
23
|
+
type SuggestionScope,
|
|
24
|
+
suggestChoices,
|
|
25
|
+
suggestionLabel,
|
|
26
|
+
} from "@/settings/suggest";
|
|
27
|
+
import type { SettingsValues, SurfaceValues } from "@/settings/types";
|
|
28
|
+
import { type ValidationScope, validateSettings } from "@/settings/validate";
|
|
29
|
+
|
|
30
|
+
/** Who asks, for which guild and in which language. */
|
|
31
|
+
export interface RequestContext {
|
|
32
|
+
readonly guildId: string;
|
|
33
|
+
readonly userId: string;
|
|
34
|
+
readonly locale: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Outcome of {@link SettingsService.validate}: the submitted fields in their
|
|
39
|
+
* stored form (an unset field maps to `undefined`), or every issue found.
|
|
40
|
+
*/
|
|
41
|
+
export type ValidationResult<D extends SettingsDeclaration> =
|
|
42
|
+
| { readonly ok: true; readonly values: Partial<SettingsValues<D>> }
|
|
43
|
+
| { readonly ok: false; readonly issues: readonly SettingsIssue[] };
|
|
44
|
+
|
|
45
|
+
/** Reads and writes the settings modules declare, per guild. */
|
|
46
|
+
export interface SettingsService {
|
|
47
|
+
/**
|
|
48
|
+
* Module logic read: every declared field, secrets included. A field with
|
|
49
|
+
* no valid stored value reads as its default. Cached per guild and module
|
|
50
|
+
* until a change of them (this service's writes, or a notifier event) or
|
|
51
|
+
* `SETTINGS_CACHE_TTL_MS`, whichever comes first.
|
|
52
|
+
*
|
|
53
|
+
* Writes only to migrate (FR-018): values stored under an older declaration
|
|
54
|
+
* version go through the declaration's `migrate`, are validated, and are
|
|
55
|
+
* written back once under the declared version, like any other write. A
|
|
56
|
+
* migration that throws or yields invalid values writes nothing and is
|
|
57
|
+
* logged; the guild then reads defaults for the fields it cannot read.
|
|
58
|
+
* Values stored under a newer version are never written (FR-019).
|
|
59
|
+
*/
|
|
60
|
+
get<D extends SettingsDeclaration>(declaration: D, guildId: string): Promise<SettingsValues<D>>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Surface read: like {@link get}, with each secret replaced by whether it
|
|
64
|
+
* is set. A secret's value never leaves the service through a surface.
|
|
65
|
+
*/
|
|
66
|
+
getForSurface<D extends SettingsDeclaration>(
|
|
67
|
+
declaration: D,
|
|
68
|
+
guildId: string,
|
|
69
|
+
): Promise<SurfaceValues<D>>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Configuration status (FR-040): the required fields without default the
|
|
73
|
+
* guild left unset, read through the same cache as {@link get}, so a write
|
|
74
|
+
* shows at once. A stored value that fails validation counts as unset.
|
|
75
|
+
*/
|
|
76
|
+
status(declaration: SettingsDeclaration, guildId: string): Promise<SettingsStatus>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate a submission without writing: the same checks as {@link set},
|
|
80
|
+
* shared by every surface.
|
|
81
|
+
*
|
|
82
|
+
* @throws ValidationError when `ctx` is for another guild than `guildId`.
|
|
83
|
+
*/
|
|
84
|
+
validate<D extends SettingsDeclaration>(
|
|
85
|
+
declaration: D,
|
|
86
|
+
guildId: string,
|
|
87
|
+
patch: unknown,
|
|
88
|
+
ctx: RequestContext,
|
|
89
|
+
): Promise<ValidationResult<D>>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate a submission, then merge it onto the stored values in one write:
|
|
93
|
+
* nothing is stored unless every field is valid. A field submitted as
|
|
94
|
+
* `null` is unset; stored fields the declaration no longer declares are
|
|
95
|
+
* dropped. Values stored under an older declaration version are migrated
|
|
96
|
+
* first, so the patch merges onto the values the guild reads. Emits a
|
|
97
|
+
* settings change once written.
|
|
98
|
+
*
|
|
99
|
+
* @returns the written record.
|
|
100
|
+
* @throws SettingsValidationError listing every invalid field.
|
|
101
|
+
* @throws ConflictError when `ctx.expectedRevision` is not the stored
|
|
102
|
+
* revision, or when the stored record was written under a newer declaration
|
|
103
|
+
* version.
|
|
104
|
+
* @throws ValidationError when `ctx` is for another guild than `guildId`.
|
|
105
|
+
*/
|
|
106
|
+
set<D extends SettingsDeclaration>(
|
|
107
|
+
declaration: D,
|
|
108
|
+
guildId: string,
|
|
109
|
+
patch: unknown,
|
|
110
|
+
ctx: RequestContext & { readonly expectedRevision?: number },
|
|
111
|
+
): Promise<StoredSettings>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Unset the given fields (or every field), so they read as their defaults.
|
|
115
|
+
* Writes nothing for a guild that never stored settings. Like
|
|
116
|
+
* {@link set}, migrates older stored values before unsetting.
|
|
117
|
+
*
|
|
118
|
+
* @throws SettingsValidationError when a key is not declared.
|
|
119
|
+
* @throws ConflictError when the stored record changed concurrently or was
|
|
120
|
+
* written under a newer declaration version.
|
|
121
|
+
* @throws ValidationError when `ctx` is for another guild than `guildId`.
|
|
122
|
+
*/
|
|
123
|
+
reset<D extends SettingsDeclaration>(
|
|
124
|
+
declaration: D,
|
|
125
|
+
guildId: string,
|
|
126
|
+
keys: readonly string[] | "all",
|
|
127
|
+
ctx: RequestContext,
|
|
128
|
+
): Promise<void>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Describe a module's settings for surfaces other than Discord (FR-030), as
|
|
132
|
+
* a JSON Schema with every text translated.
|
|
133
|
+
*
|
|
134
|
+
* @param locale - any locale string; an unsupported one falls back to English.
|
|
135
|
+
*/
|
|
136
|
+
describe(declaration: SettingsDeclaration, locale: string): SettingsSchema;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Suggestions for one field (FR-021 to FR-026), in the command
|
|
140
|
+
* autocomplete's own `Choice` format, so one search serves both. A channel,
|
|
141
|
+
* role or member field searches the guild; static choices are translated
|
|
142
|
+
* to `ctx.locale` and kept when their name or value contains `query`; a
|
|
143
|
+
* dynamic search gets `query` and `ctx` (with `ctx.values`, the other
|
|
144
|
+
* values currently entered). A list suggests for its item. At most 25
|
|
145
|
+
* results; a search slower than `SUGGESTION_TIMEOUT_MS` (2.5 s) or failing
|
|
146
|
+
* yields none and is logged at `warn`.
|
|
147
|
+
*
|
|
148
|
+
* @throws ValidationError when `fieldKey` is not declared: a malformed
|
|
149
|
+
* request, like a patch that is not an object.
|
|
150
|
+
*/
|
|
151
|
+
suggest<D extends SettingsDeclaration>(
|
|
152
|
+
declaration: D,
|
|
153
|
+
fieldKey: string,
|
|
154
|
+
query: string,
|
|
155
|
+
ctx: RequestContext & { readonly values: Readonly<Record<string, unknown>> },
|
|
156
|
+
): Promise<readonly Choice[]>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The readable label of one value of a field, in `ctx.locale` (FR-022): a
|
|
160
|
+
* channel, role or member's name on the guild, a static choice's
|
|
161
|
+
* translated label, a dynamic search's `label`, or, without one, the name
|
|
162
|
+
* of the exact match in `resolve(String(value))`. A list reads `value` as
|
|
163
|
+
* one item. A search gets the guild's current values as `ctx.values`.
|
|
164
|
+
*
|
|
165
|
+
* @returns the label, or `undefined` when none is known (the value is not
|
|
166
|
+
* suggested, the entity is gone, or the search timed out or failed, logged).
|
|
167
|
+
* @throws ValidationError when `fieldKey` is not declared.
|
|
168
|
+
*/
|
|
169
|
+
label<D extends SettingsDeclaration>(
|
|
170
|
+
declaration: D,
|
|
171
|
+
fieldKey: string,
|
|
172
|
+
value: unknown,
|
|
173
|
+
ctx: RequestContext,
|
|
174
|
+
): Promise<string | undefined>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface SettingsServiceDeps {
|
|
178
|
+
readonly registry: SettingsRegistry;
|
|
179
|
+
readonly store: SettingsStore;
|
|
180
|
+
readonly guilds: GuildDirectory;
|
|
181
|
+
readonly notifier: SettingsChangedNotifier;
|
|
182
|
+
readonly translator: Translator;
|
|
183
|
+
readonly clock: Clock;
|
|
184
|
+
readonly logger: Logger;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create the settings service over its ports.
|
|
189
|
+
*
|
|
190
|
+
* @param deps - the registry of declarations and the ports the service reads,
|
|
191
|
+
* writes, notifies and logs through.
|
|
192
|
+
* @returns the service shared by module logic and every configuration surface.
|
|
193
|
+
*/
|
|
194
|
+
export function createSettingsService(deps: SettingsServiceDeps): SettingsService {
|
|
195
|
+
const { store, guilds, notifier, translator, clock, logger } = deps;
|
|
196
|
+
const cache = createSettingsCache({ clock, notifier });
|
|
197
|
+
const ports: SuggestionPorts = { guilds, translator, logger };
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* What a guild reads of `stored`: its valid declared fields, defaults not
|
|
201
|
+
* applied, migrated in memory when stored under an older version. Logs
|
|
202
|
+
* every field it has to drop and every failed migration.
|
|
203
|
+
*
|
|
204
|
+
* @returns the values, and whether they are a successful migration still to
|
|
205
|
+
* be written back.
|
|
206
|
+
*/
|
|
207
|
+
function readable(
|
|
208
|
+
declaration: SettingsDeclaration,
|
|
209
|
+
stored: StoredSettings | null,
|
|
210
|
+
): { values: Readonly<Record<string, unknown>>; migrated: boolean } {
|
|
211
|
+
if (stored === null) {
|
|
212
|
+
return { values: {}, migrated: false };
|
|
213
|
+
}
|
|
214
|
+
const at = { guildId: stored.guildId, moduleId: declaration.id };
|
|
215
|
+
if (stored.version < declaration.version) {
|
|
216
|
+
const outcome = migrateStored(declaration, stored);
|
|
217
|
+
if (!outcome.ok) {
|
|
218
|
+
logger.error(
|
|
219
|
+
{
|
|
220
|
+
...at,
|
|
221
|
+
fromVersion: stored.version,
|
|
222
|
+
toVersion: declaration.version,
|
|
223
|
+
keys: outcome.invalidKeys,
|
|
224
|
+
...(outcome.error === undefined ? {} : { err: errorMessage(outcome.error) }),
|
|
225
|
+
},
|
|
226
|
+
"Settings migration failed; storage left unchanged, reading defaults for the fields it cannot read",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return { values: outcome.values, migrated: outcome.ok };
|
|
230
|
+
}
|
|
231
|
+
const decoded = decodeStored(declaration, stored.values);
|
|
232
|
+
if (decoded.invalidKeys.length > 0) {
|
|
233
|
+
logger.error(
|
|
234
|
+
{ ...at, keys: decoded.invalidKeys },
|
|
235
|
+
"Stored settings failed validation; reading defaults for these fields",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return { values: decoded.values, migrated: false };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Write a successful migration back, expecting the revision it was read at.
|
|
243
|
+
* When another reader (another shard) wrote first, its record is read
|
|
244
|
+
* again instead: the conflict is not the caller's to handle.
|
|
245
|
+
*/
|
|
246
|
+
async function writeBack(params: {
|
|
247
|
+
declaration: SettingsDeclaration;
|
|
248
|
+
stored: StoredSettings;
|
|
249
|
+
values: Readonly<Record<string, unknown>>;
|
|
250
|
+
}): Promise<Readonly<Record<string, unknown>>> {
|
|
251
|
+
const { declaration, stored, values } = params;
|
|
252
|
+
const keys = new Set([...Object.keys(stored.values), ...Object.keys(values)]);
|
|
253
|
+
const changedKeys = [...keys].filter(
|
|
254
|
+
(key) => !isDeepStrictEqual(stored.values[key], values[key]),
|
|
255
|
+
);
|
|
256
|
+
try {
|
|
257
|
+
await write({
|
|
258
|
+
declaration,
|
|
259
|
+
guildId: stored.guildId,
|
|
260
|
+
stored,
|
|
261
|
+
expectedRevision: stored.revision,
|
|
262
|
+
values,
|
|
263
|
+
changedKeys,
|
|
264
|
+
author: undefined,
|
|
265
|
+
});
|
|
266
|
+
return values;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof ConflictError) {
|
|
269
|
+
const latest = await store.read(stored.guildId, declaration.id);
|
|
270
|
+
return readable(declaration, latest).values;
|
|
271
|
+
}
|
|
272
|
+
logger.error(
|
|
273
|
+
{
|
|
274
|
+
guildId: stored.guildId,
|
|
275
|
+
moduleId: declaration.id,
|
|
276
|
+
fromVersion: stored.version,
|
|
277
|
+
toVersion: declaration.version,
|
|
278
|
+
err: errorMessage(error),
|
|
279
|
+
},
|
|
280
|
+
"Failed to write migrated settings back; reading the migrated values",
|
|
281
|
+
);
|
|
282
|
+
return values;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Read and decode a guild's stored settings, bypassing the cache. */
|
|
287
|
+
async function load<D extends SettingsDeclaration>(
|
|
288
|
+
declaration: D,
|
|
289
|
+
guildId: string,
|
|
290
|
+
): Promise<SettingsValues<D>> {
|
|
291
|
+
const stored = await store.read(guildId, declaration.id);
|
|
292
|
+
const read = readable(declaration, stored);
|
|
293
|
+
const values =
|
|
294
|
+
read.migrated && stored !== null
|
|
295
|
+
? await writeBack({ declaration, stored, values: read.values })
|
|
296
|
+
: read.values;
|
|
297
|
+
const withDefaults = Object.entries(declaration.fields).map(([key, declared]) => [
|
|
298
|
+
key,
|
|
299
|
+
Object.hasOwn(values, key) ? values[key] : declared.default,
|
|
300
|
+
]);
|
|
301
|
+
return Object.fromEntries(withDefaults) as SettingsValues<D>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function get<D extends SettingsDeclaration>(
|
|
305
|
+
declaration: D,
|
|
306
|
+
guildId: string,
|
|
307
|
+
): Promise<SettingsValues<D>> {
|
|
308
|
+
return cache.read(guildId, declaration.id, () => load(declaration, guildId));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Write `values` over `stored` under the declared version and tell
|
|
313
|
+
* listeners which keys changed. `author` is the user who asked, or
|
|
314
|
+
* `undefined` for a write the kernel makes on its own (a migration).
|
|
315
|
+
*/
|
|
316
|
+
async function write(params: {
|
|
317
|
+
declaration: SettingsDeclaration;
|
|
318
|
+
guildId: string;
|
|
319
|
+
stored: StoredSettings | null;
|
|
320
|
+
expectedRevision: number | null;
|
|
321
|
+
values: Readonly<Record<string, unknown>>;
|
|
322
|
+
changedKeys: readonly string[];
|
|
323
|
+
author: string | undefined;
|
|
324
|
+
}): Promise<StoredSettings> {
|
|
325
|
+
const { declaration, guildId, stored, author } = params;
|
|
326
|
+
const next: StoredSettings = {
|
|
327
|
+
guildId,
|
|
328
|
+
moduleId: declaration.id,
|
|
329
|
+
version: declaration.version,
|
|
330
|
+
revision: (stored?.revision ?? 0) + 1,
|
|
331
|
+
values: params.values,
|
|
332
|
+
updatedAt: clock.now().toISOString(),
|
|
333
|
+
...(author === undefined ? {} : { updatedBy: author }),
|
|
334
|
+
};
|
|
335
|
+
await store.write(next, { expectedRevision: params.expectedRevision });
|
|
336
|
+
// Before notifying: a listener that reads back must see the new values.
|
|
337
|
+
cache.invalidate(guildId, declaration.id);
|
|
338
|
+
notifier.notify({
|
|
339
|
+
guildId,
|
|
340
|
+
moduleId: declaration.id,
|
|
341
|
+
changedKeys: params.changedKeys,
|
|
342
|
+
revision: next.revision,
|
|
343
|
+
...(author === undefined ? {} : { changedBy: author }),
|
|
344
|
+
});
|
|
345
|
+
return next;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function getForSurface<D extends SettingsDeclaration>(
|
|
349
|
+
declaration: D,
|
|
350
|
+
guildId: string,
|
|
351
|
+
): Promise<SurfaceValues<D>> {
|
|
352
|
+
const values: Record<string, unknown> = await get(declaration, guildId);
|
|
353
|
+
const surface = Object.entries(values).map(([key, value]) =>
|
|
354
|
+
declaration.fields[key]?.spec.kind === "secret"
|
|
355
|
+
? [key, { isSet: value !== undefined }]
|
|
356
|
+
: [key, value],
|
|
357
|
+
);
|
|
358
|
+
return Object.fromEntries(surface) as SurfaceValues<D>;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** What a strict field's search is checked with while validating a submission. */
|
|
362
|
+
function validationScope(declaration: SettingsDeclaration, ctx: RequestContext): ValidationScope {
|
|
363
|
+
return {
|
|
364
|
+
ports,
|
|
365
|
+
guildId: ctx.guildId,
|
|
366
|
+
userId: ctx.userId,
|
|
367
|
+
locale: ctx.locale,
|
|
368
|
+
currentValues: () => getForSurface(declaration, ctx.guildId),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
get,
|
|
374
|
+
|
|
375
|
+
getForSurface,
|
|
376
|
+
|
|
377
|
+
async status(declaration: SettingsDeclaration, guildId: string): Promise<SettingsStatus> {
|
|
378
|
+
return settingsStatus(declaration, await get(declaration, guildId));
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
async validate<D extends SettingsDeclaration>(
|
|
382
|
+
declaration: D,
|
|
383
|
+
guildId: string,
|
|
384
|
+
patch: unknown,
|
|
385
|
+
ctx: RequestContext,
|
|
386
|
+
): Promise<ValidationResult<D>> {
|
|
387
|
+
assertSameGuild(guildId, ctx);
|
|
388
|
+
const scope = validationScope(declaration, ctx);
|
|
389
|
+
const result = await validateSettings({ declaration, patch, scope });
|
|
390
|
+
return result as ValidationResult<D>;
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
async set<D extends SettingsDeclaration>(
|
|
394
|
+
declaration: D,
|
|
395
|
+
guildId: string,
|
|
396
|
+
patch: unknown,
|
|
397
|
+
ctx: RequestContext & { readonly expectedRevision?: number },
|
|
398
|
+
): Promise<StoredSettings> {
|
|
399
|
+
assertSameGuild(guildId, ctx);
|
|
400
|
+
const scope = validationScope(declaration, ctx);
|
|
401
|
+
const result = await validateSettings({ declaration, patch, scope });
|
|
402
|
+
if (!result.ok) {
|
|
403
|
+
throw new SettingsValidationError(result.issues);
|
|
404
|
+
}
|
|
405
|
+
const stored = await store.read(guildId, declaration.id);
|
|
406
|
+
assertWritable(declaration, stored);
|
|
407
|
+
const values = { ...readable(declaration, stored).values };
|
|
408
|
+
for (const [key, value] of Object.entries(result.values)) {
|
|
409
|
+
if (value === undefined) {
|
|
410
|
+
delete values[key];
|
|
411
|
+
} else {
|
|
412
|
+
values[key] = value;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return write({
|
|
416
|
+
declaration,
|
|
417
|
+
guildId,
|
|
418
|
+
stored,
|
|
419
|
+
expectedRevision:
|
|
420
|
+
ctx.expectedRevision === undefined ? (stored?.revision ?? null) : ctx.expectedRevision,
|
|
421
|
+
values,
|
|
422
|
+
changedKeys: Object.keys(result.values),
|
|
423
|
+
author: ctx.userId,
|
|
424
|
+
});
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
async reset<D extends SettingsDeclaration>(
|
|
428
|
+
declaration: D,
|
|
429
|
+
guildId: string,
|
|
430
|
+
keys: readonly string[] | "all",
|
|
431
|
+
ctx: RequestContext,
|
|
432
|
+
): Promise<void> {
|
|
433
|
+
assertSameGuild(guildId, ctx);
|
|
434
|
+
const declaredKeys = Object.keys(declaration.fields);
|
|
435
|
+
const resetKeys = keys === "all" ? declaredKeys : keys;
|
|
436
|
+
const issues = resetKeys.filter((key) => !declaredKeys.includes(key)).map(unknownField);
|
|
437
|
+
if (issues.length > 0) {
|
|
438
|
+
throw new SettingsValidationError(issues);
|
|
439
|
+
}
|
|
440
|
+
const stored = await store.read(guildId, declaration.id);
|
|
441
|
+
if (stored === null) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
assertWritable(declaration, stored);
|
|
445
|
+
const values = { ...readable(declaration, stored).values };
|
|
446
|
+
for (const key of resetKeys) {
|
|
447
|
+
delete values[key];
|
|
448
|
+
}
|
|
449
|
+
await write({
|
|
450
|
+
declaration,
|
|
451
|
+
guildId,
|
|
452
|
+
stored,
|
|
453
|
+
expectedRevision: stored.revision,
|
|
454
|
+
values,
|
|
455
|
+
changedKeys: resetKeys,
|
|
456
|
+
author: ctx.userId,
|
|
457
|
+
});
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
describe(declaration: SettingsDeclaration, locale: string): SettingsSchema {
|
|
461
|
+
return describeSettings({ declaration, locale, translator });
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
async suggest<D extends SettingsDeclaration>(
|
|
465
|
+
declaration: D,
|
|
466
|
+
fieldKey: string,
|
|
467
|
+
query: string,
|
|
468
|
+
ctx: RequestContext & { readonly values: Readonly<Record<string, unknown>> },
|
|
469
|
+
): Promise<readonly Choice[]> {
|
|
470
|
+
const spec = declaredField(declaration, fieldKey).spec;
|
|
471
|
+
const scope: SuggestionScope = { ...ports, moduleId: declaration.id, key: fieldKey, ctx };
|
|
472
|
+
return suggestChoices({ scope, spec, query });
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
async label<D extends SettingsDeclaration>(
|
|
476
|
+
declaration: D,
|
|
477
|
+
fieldKey: string,
|
|
478
|
+
value: unknown,
|
|
479
|
+
ctx: RequestContext,
|
|
480
|
+
): Promise<string | undefined> {
|
|
481
|
+
const spec = declaredField(declaration, fieldKey).spec;
|
|
482
|
+
const values = await getForSurface(declaration, ctx.guildId);
|
|
483
|
+
const scope: SuggestionScope = {
|
|
484
|
+
...ports,
|
|
485
|
+
moduleId: declaration.id,
|
|
486
|
+
key: fieldKey,
|
|
487
|
+
ctx: { ...ctx, values },
|
|
488
|
+
};
|
|
489
|
+
return suggestionLabel({ scope, spec, value });
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* A surface acts for the guild of its request only: a context of another
|
|
496
|
+
* guild is a malformed request, rejected before anything is read or written.
|
|
497
|
+
*/
|
|
498
|
+
function assertSameGuild(guildId: string, ctx: RequestContext): void {
|
|
499
|
+
if (ctx.guildId !== guildId) {
|
|
500
|
+
throw new ValidationError(
|
|
501
|
+
`Settings request for guild ${ctx.guildId} cannot target guild ${guildId}`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** A record written under a newer declaration version is never overwritten (FR-019). */
|
|
507
|
+
function assertWritable(declaration: SettingsDeclaration, stored: StoredSettings | null): void {
|
|
508
|
+
if (stored !== null && stored.version > declaration.version) {
|
|
509
|
+
throw new ConflictError(
|
|
510
|
+
`Settings of module "${declaration.id}" in guild ${stored.guildId} were written under version ${stored.version}, newer than the declared version ${declaration.version}`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* The declared field under `key`.
|
|
517
|
+
*
|
|
518
|
+
* @throws ValidationError when the declaration has none: asking a field's
|
|
519
|
+
* suggestions or labels by a wrong key is a malformed request.
|
|
520
|
+
*/
|
|
521
|
+
function declaredField(declaration: SettingsDeclaration, key: string): AnyField {
|
|
522
|
+
const declared = Object.hasOwn(declaration.fields, key) ? declaration.fields[key] : undefined;
|
|
523
|
+
if (declared === undefined) {
|
|
524
|
+
throw new ValidationError(`Settings of module "${declaration.id}" have no field "${key}"`);
|
|
525
|
+
}
|
|
526
|
+
return declared;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function unknownField(key: string): SettingsIssue {
|
|
530
|
+
return {
|
|
531
|
+
field: key,
|
|
532
|
+
code: "unknownField",
|
|
533
|
+
translation: { key: SETTINGS_ISSUE_MESSAGES.unknownField },
|
|
534
|
+
};
|
|
535
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ErrorTranslation, ValidationError } from "@/errors/business-error";
|
|
2
|
+
|
|
3
|
+
/** Why one submitted settings value was rejected. */
|
|
4
|
+
export type SettingsIssueCode =
|
|
5
|
+
| "required"
|
|
6
|
+
| "type"
|
|
7
|
+
| "min"
|
|
8
|
+
| "max"
|
|
9
|
+
| "minLength"
|
|
10
|
+
| "maxLength"
|
|
11
|
+
| "minItems"
|
|
12
|
+
| "maxItems"
|
|
13
|
+
| "duplicate"
|
|
14
|
+
| "channelType"
|
|
15
|
+
| "notFound"
|
|
16
|
+
| "unknownChoice"
|
|
17
|
+
| "unknownField";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One rejected value of a settings submission. `field` is the field key, a
|
|
21
|
+
* `key[index]` path for a list item (e.g. `pingRoles[2]`), or a `key.name` path
|
|
22
|
+
* for a toggle (e.g. `modules.music`); `translation` points at the settings
|
|
23
|
+
* catalog so every surface shows the same wording for the same failure.
|
|
24
|
+
*/
|
|
25
|
+
export interface SettingsIssue {
|
|
26
|
+
readonly field: string;
|
|
27
|
+
readonly code: SettingsIssueCode;
|
|
28
|
+
readonly translation: ErrorTranslation;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A settings submission failed validation. Carries every issue at once, so a
|
|
33
|
+
* surface can flag all faulty fields in one round trip; nothing was stored.
|
|
34
|
+
*/
|
|
35
|
+
export class SettingsValidationError extends ValidationError {
|
|
36
|
+
constructor(readonly issues: readonly SettingsIssue[]) {
|
|
37
|
+
super(
|
|
38
|
+
`Invalid settings: ${issues.map((issue) => `${issue.field} (${issue.code})`).join(", ")}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SettingsDeclaration } from "@/settings/define-settings";
|
|
2
|
+
|
|
3
|
+
/** Whether a module can run on a guild: what an administrator still has to set. */
|
|
4
|
+
export interface SettingsStatus {
|
|
5
|
+
/** Keys of the required fields without default the guild left unset, in declared order. */
|
|
6
|
+
readonly missing: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The configuration status of a module's values on one guild (FR-040). A
|
|
11
|
+
* required field with a default always has a value, so it is never missing.
|
|
12
|
+
*
|
|
13
|
+
* @param declaration - the module's settings declaration.
|
|
14
|
+
* @param values - the guild's values as the service reads them: an unset (or
|
|
15
|
+
* invalid) field is `undefined` unless it has a default.
|
|
16
|
+
*/
|
|
17
|
+
export function settingsStatus(
|
|
18
|
+
declaration: SettingsDeclaration,
|
|
19
|
+
values: Readonly<Record<string, unknown>>,
|
|
20
|
+
): SettingsStatus {
|
|
21
|
+
const missing = Object.entries(declaration.fields)
|
|
22
|
+
.filter(([key, declared]) => declared.required && values[key] === undefined)
|
|
23
|
+
.map(([key]) => key);
|
|
24
|
+
return { missing };
|
|
25
|
+
}
|