@endora-commerce/mod-settings 0.100.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/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/admin/api/settings-client.d.ts +47 -0
- package/dist/admin/api/settings-client.d.ts.map +1 -0
- package/dist/admin/api/settings-client.js +41 -0
- package/dist/admin/api/settings-client.js.map +1 -0
- package/dist/admin/components/ActivationPointerRow.d.ts +21 -0
- package/dist/admin/components/ActivationPointerRow.d.ts.map +1 -0
- package/dist/admin/components/ActivationPointerRow.js +28 -0
- package/dist/admin/components/ActivationPointerRow.js.map +1 -0
- package/dist/admin/components/AssetIdSettingInput.d.ts +12 -0
- package/dist/admin/components/AssetIdSettingInput.d.ts.map +1 -0
- package/dist/admin/components/AssetIdSettingInput.js +10 -0
- package/dist/admin/components/AssetIdSettingInput.js.map +1 -0
- package/dist/admin/components/ConfigurationReferenceInput.d.ts +39 -0
- package/dist/admin/components/ConfigurationReferenceInput.d.ts.map +1 -0
- package/dist/admin/components/ConfigurationReferenceInput.js +74 -0
- package/dist/admin/components/ConfigurationReferenceInput.js.map +1 -0
- package/dist/admin/components/ConflictBanner.d.ts +13 -0
- package/dist/admin/components/ConflictBanner.d.ts.map +1 -0
- package/dist/admin/components/ConflictBanner.js +12 -0
- package/dist/admin/components/ConflictBanner.js.map +1 -0
- package/dist/admin/components/ImageSettingInput.d.ts +34 -0
- package/dist/admin/components/ImageSettingInput.d.ts.map +1 -0
- package/dist/admin/components/ImageSettingInput.js +36 -0
- package/dist/admin/components/ImageSettingInput.js.map +1 -0
- package/dist/admin/components/QuoteRequestsSettingsTab.d.ts +3 -0
- package/dist/admin/components/QuoteRequestsSettingsTab.d.ts.map +1 -0
- package/dist/admin/components/QuoteRequestsSettingsTab.js +69 -0
- package/dist/admin/components/QuoteRequestsSettingsTab.js.map +1 -0
- package/dist/admin/components/SellerCompanyDataInput.d.ts +22 -0
- package/dist/admin/components/SellerCompanyDataInput.d.ts.map +1 -0
- package/dist/admin/components/SellerCompanyDataInput.js +53 -0
- package/dist/admin/components/SellerCompanyDataInput.js.map +1 -0
- package/dist/admin/components/SettingRowEditor.d.ts +55 -0
- package/dist/admin/components/SettingRowEditor.d.ts.map +1 -0
- package/dist/admin/components/SettingRowEditor.js +160 -0
- package/dist/admin/components/SettingRowEditor.js.map +1 -0
- package/dist/admin/index.d.ts +39 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +87 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/CachePage.d.ts +14 -0
- package/dist/admin/pages/CachePage.d.ts.map +1 -0
- package/dist/admin/pages/CachePage.js +124 -0
- package/dist/admin/pages/CachePage.js.map +1 -0
- package/dist/admin/pages/GroupsPage.d.ts +17 -0
- package/dist/admin/pages/GroupsPage.d.ts.map +1 -0
- package/dist/admin/pages/GroupsPage.js +84 -0
- package/dist/admin/pages/GroupsPage.js.map +1 -0
- package/dist/admin/pages/SettingsPage.d.ts +20 -0
- package/dist/admin/pages/SettingsPage.d.ts.map +1 -0
- package/dist/admin/pages/SettingsPage.js +453 -0
- package/dist/admin/pages/SettingsPage.js.map +1 -0
- package/dist/backend/cli/cache-clear.d.ts +23 -0
- package/dist/backend/cli/cache-clear.d.ts.map +1 -0
- package/dist/backend/cli/cache-clear.js +37 -0
- package/dist/backend/cli/cache-clear.js.map +1 -0
- package/dist/backend/index.d.ts +100 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +125 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +308 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +188 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.cache.d.ts +20 -0
- package/dist/backend/routes.cache.d.ts.map +1 -0
- package/dist/backend/routes.cache.js +22 -0
- package/dist/backend/routes.cache.js.map +1 -0
- package/dist/backend/routes.homepage.d.ts +15 -0
- package/dist/backend/routes.homepage.d.ts.map +1 -0
- package/dist/backend/routes.homepage.js +20 -0
- package/dist/backend/routes.homepage.js.map +1 -0
- package/dist/backend/routes.product-card-buttons.d.ts +15 -0
- package/dist/backend/routes.product-card-buttons.d.ts.map +1 -0
- package/dist/backend/routes.product-card-buttons.js +21 -0
- package/dist/backend/routes.product-card-buttons.js.map +1 -0
- package/dist/backend/routes.speculation-rules.d.ts +16 -0
- package/dist/backend/routes.speculation-rules.d.ts.map +1 -0
- package/dist/backend/routes.speculation-rules.js +21 -0
- package/dist/backend/routes.speculation-rules.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +15 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +20 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/cache-admin.service.d.ts +83 -0
- package/dist/backend/services/cache-admin.service.d.ts.map +1 -0
- package/dist/backend/services/cache-admin.service.js +148 -0
- package/dist/backend/services/cache-admin.service.js.map +1 -0
- package/dist/backend/services/homepage-resolver.d.ts +25 -0
- package/dist/backend/services/homepage-resolver.d.ts.map +1 -0
- package/dist/backend/services/homepage-resolver.js +31 -0
- package/dist/backend/services/homepage-resolver.js.map +1 -0
- package/dist/backend/services/product-card-buttons-resolver.d.ts +25 -0
- package/dist/backend/services/product-card-buttons-resolver.d.ts.map +1 -0
- package/dist/backend/services/product-card-buttons-resolver.js +37 -0
- package/dist/backend/services/product-card-buttons-resolver.js.map +1 -0
- package/dist/backend/services/registered-settings-manifests.d.ts +45 -0
- package/dist/backend/services/registered-settings-manifests.d.ts.map +1 -0
- package/dist/backend/services/registered-settings-manifests.js +55 -0
- package/dist/backend/services/registered-settings-manifests.js.map +1 -0
- package/dist/backend/services/setting-write-validators.d.ts +43 -0
- package/dist/backend/services/setting-write-validators.d.ts.map +1 -0
- package/dist/backend/services/setting-write-validators.js +43 -0
- package/dist/backend/services/setting-write-validators.js.map +1 -0
- package/dist/backend/services/settings-admin.service.d.ts +206 -0
- package/dist/backend/services/settings-admin.service.d.ts.map +1 -0
- package/dist/backend/services/settings-admin.service.js +642 -0
- package/dist/backend/services/settings-admin.service.js.map +1 -0
- package/dist/backend/services/shop-info-resolver.d.ts +15 -0
- package/dist/backend/services/shop-info-resolver.d.ts.map +1 -0
- package/dist/backend/services/shop-info-resolver.js +44 -0
- package/dist/backend/services/shop-info-resolver.js.map +1 -0
- package/dist/backend/services/speculation-rules-resolver.d.ts +28 -0
- package/dist/backend/services/speculation-rules-resolver.d.ts.map +1 -0
- package/dist/backend/services/speculation-rules-resolver.js +39 -0
- package/dist/backend/services/speculation-rules-resolver.js.map +1 -0
- package/dist/manifest.d.ts +194 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +379 -0
- package/dist/manifest.js.map +1 -0
- package/docs/settings/index.md +171 -0
- package/i18n/en.json +167 -0
- package/i18n/pl.json +167 -0
- package/package.json +96 -0
- package/tailwind.css +14 -0
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { defineModuleManifest, defineModuleSettingsManifest, } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Built-in settings manifest for the settings module itself — feature 004.
|
|
4
|
+
*
|
|
5
|
+
* Seeds the platform-wide `general` group on every backend boot so other
|
|
6
|
+
* modules' manifests can default-attach to it without a chicken-and-egg
|
|
7
|
+
* problem. The group is `isSystemProtected: true`, which the admin service
|
|
8
|
+
* refuses to delete.
|
|
9
|
+
*/
|
|
10
|
+
const settings = defineModuleSettingsManifest({
|
|
11
|
+
moduleCode: 'settings',
|
|
12
|
+
groups: [
|
|
13
|
+
{
|
|
14
|
+
code: 'general',
|
|
15
|
+
name: 'General',
|
|
16
|
+
isSystemProtected: true,
|
|
17
|
+
// Empty salesChannelCodes ⇒ applies to every sales channel.
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
// Shop / company contact information surfaced across the storefront
|
|
21
|
+
// (footer, 404 "need help?" block, contact form recipients, etc.).
|
|
22
|
+
code: 'shop',
|
|
23
|
+
name: 'Shop information',
|
|
24
|
+
// Empty salesChannelCodes ⇒ applies to every sales channel; values can
|
|
25
|
+
// still be overridden per channel via the standard scope mechanism.
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
// Storefront-template behaviour toggles (performance hints, etc.).
|
|
29
|
+
code: 'storefront',
|
|
30
|
+
name: 'Storefront',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
settings: [
|
|
34
|
+
{
|
|
35
|
+
// Image URL shown when a product has no image of its own, on product
|
|
36
|
+
// cards / listings (and the product page). Resolvable globally or
|
|
37
|
+
// per sales channel via the standard settings scope mechanism.
|
|
38
|
+
code: 'product_image_placeholder_url',
|
|
39
|
+
name: 'Product image placeholder',
|
|
40
|
+
description: 'Image displayed on product cards and listings when a product has no ' +
|
|
41
|
+
'image of its own. Upload a file (drag-and-drop or file picker) or enter ' +
|
|
42
|
+
'an image URL. Leave empty to show no placeholder. Can be overridden per ' +
|
|
43
|
+
'sales channel.',
|
|
44
|
+
groupCode: 'general',
|
|
45
|
+
valueType: 'string',
|
|
46
|
+
defaultValue: '',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
// Minutes of inactivity after which an admin is signed out of the Admin
|
|
50
|
+
// UI. Enforced client-side by an idle timer in the admin app.
|
|
51
|
+
code: 'admin.idle_logout_minutes',
|
|
52
|
+
name: 'Admin idle logout (minutes)',
|
|
53
|
+
description: 'Number of minutes of inactivity after which an administrator is ' +
|
|
54
|
+
'automatically signed out of the Admin UI. Default 60.',
|
|
55
|
+
groupCode: 'general',
|
|
56
|
+
valueType: 'number',
|
|
57
|
+
defaultValue: 60,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
// Slug (URL path) of the CMS page to serve as the storefront home page.
|
|
61
|
+
// Empty ⇒ the storefront falls back to its built-in landing page. Can be
|
|
62
|
+
// overridden per sales channel.
|
|
63
|
+
code: 'homepage_cms_page_slug',
|
|
64
|
+
name: 'Home page CMS page',
|
|
65
|
+
description: 'Slug (URL path) of the CMS page to use as the storefront home page, ' +
|
|
66
|
+
'e.g. "welcome". Leave empty to use the built-in landing page. Can be ' +
|
|
67
|
+
'overridden per sales channel.',
|
|
68
|
+
groupCode: 'general',
|
|
69
|
+
valueType: 'string',
|
|
70
|
+
defaultValue: '',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
code: 'shop.name',
|
|
74
|
+
name: 'Shop name',
|
|
75
|
+
description: 'Public name of the shop, shown across the storefront.',
|
|
76
|
+
groupCode: 'shop',
|
|
77
|
+
valueType: 'string',
|
|
78
|
+
defaultValue: '',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
code: 'shop.address',
|
|
82
|
+
name: 'Shop address',
|
|
83
|
+
description: 'Postal address of the shop, shown across the storefront.',
|
|
84
|
+
groupCode: 'shop',
|
|
85
|
+
valueType: 'string',
|
|
86
|
+
defaultValue: '',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
code: 'shop.contact_email',
|
|
90
|
+
name: 'Main contact email',
|
|
91
|
+
description: 'Primary email address used for general contact.',
|
|
92
|
+
groupCode: 'shop',
|
|
93
|
+
valueType: 'string',
|
|
94
|
+
defaultValue: '',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
code: 'shop.support_email',
|
|
98
|
+
name: 'Support / customer service email',
|
|
99
|
+
description: 'Email address of the support / customer service desk. Shown to ' +
|
|
100
|
+
'customers when they need help (e.g. on the 404 page).',
|
|
101
|
+
groupCode: 'shop',
|
|
102
|
+
valueType: 'string',
|
|
103
|
+
defaultValue: '',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
code: 'shop.phone',
|
|
107
|
+
name: 'Shop phone number',
|
|
108
|
+
description: 'Contact phone number for the shop. May be left empty.',
|
|
109
|
+
groupCode: 'shop',
|
|
110
|
+
valueType: 'string',
|
|
111
|
+
defaultValue: '',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
code: 'shop.contact_form_recipient_emails',
|
|
115
|
+
name: 'Contact form recipient emails',
|
|
116
|
+
description: 'Recipient email address(es) for the storefront contact form. ' +
|
|
117
|
+
'Multiple addresses can be provided, separated by commas.',
|
|
118
|
+
groupCode: 'shop',
|
|
119
|
+
valueType: 'string',
|
|
120
|
+
defaultValue: '',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
// Speculation Rules (prerender/prefetch) hint emitted by the storefront
|
|
124
|
+
// template for near-instant navigations.
|
|
125
|
+
code: 'storefront.speculation_rules.enabled',
|
|
126
|
+
name: 'Enable Speculation Rules',
|
|
127
|
+
description: 'Emit a Speculation Rules script in the storefront so the browser can ' +
|
|
128
|
+
'prerender/prefetch likely next pages for near-instant navigation. ' +
|
|
129
|
+
'Note: this setting only has an effect if the active Storefront UI ' +
|
|
130
|
+
'theme supports the Speculation Rules mechanism; themes that do not ' +
|
|
131
|
+
'implement it will ignore the toggle.',
|
|
132
|
+
groupCode: 'storefront',
|
|
133
|
+
valueType: 'boolean',
|
|
134
|
+
defaultValue: true,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
// Eagerness level for the Speculation Rules above. "moderate" prerenders
|
|
138
|
+
// on hover/pointer intent — the recommended balance between instant
|
|
139
|
+
// navigation and resource use; "eager" speculates aggressively on every
|
|
140
|
+
// eligible link, "conservative" only on pointer-down.
|
|
141
|
+
code: 'storefront.speculation_rules.eagerness',
|
|
142
|
+
name: 'Speculation Rules eagerness',
|
|
143
|
+
description: 'Eagerness for the storefront Speculation Rules: "conservative" ' +
|
|
144
|
+
'(on pointer-down), "moderate" (on hover — recommended), or "eager" ' +
|
|
145
|
+
'(as soon as links are discovered). Only applies when Speculation ' +
|
|
146
|
+
'Rules are enabled and supported by the active Storefront UI theme.',
|
|
147
|
+
groupCode: 'storefront',
|
|
148
|
+
valueType: 'string',
|
|
149
|
+
defaultValue: 'moderate',
|
|
150
|
+
enumOptions: ['conservative', 'moderate', 'eager'],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
// Whether the storefront product card shows an "Add to cart" button.
|
|
154
|
+
code: 'storefront.product_card.show_add_to_cart',
|
|
155
|
+
name: 'Show "Add to cart" on product cards',
|
|
156
|
+
description: 'Toggles the "Add to cart" button on storefront product card listings. ' +
|
|
157
|
+
'Only has an effect if the active Storefront UI theme renders the button.',
|
|
158
|
+
groupCode: 'general',
|
|
159
|
+
valueType: 'boolean',
|
|
160
|
+
defaultValue: true,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
// Whether the storefront product card shows an "Add to shopping list"
|
|
164
|
+
// button (adds the product to the customer's default shopping list).
|
|
165
|
+
code: 'storefront.product_card.show_add_to_shopping_list',
|
|
166
|
+
name: 'Show "Add to shopping list" on product cards',
|
|
167
|
+
description: 'Toggles the "Add to shopping list" button on storefront product card ' +
|
|
168
|
+
'listings; clicking it adds the product to the customer\'s default ' +
|
|
169
|
+
'shopping list. Only has an effect if the active Storefront UI theme ' +
|
|
170
|
+
'renders the button.',
|
|
171
|
+
groupCode: 'general',
|
|
172
|
+
valueType: 'boolean',
|
|
173
|
+
defaultValue: true,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
/** Module-lifecycle manifest (feature 018) + i18n bundle declaration (feature 019). */
|
|
178
|
+
export const manifest = defineModuleManifest({
|
|
179
|
+
id: 'settings',
|
|
180
|
+
name: 'Settings',
|
|
181
|
+
description: 'Per-module setting registry, admin UI, and value resolver.',
|
|
182
|
+
version: '1.0.0',
|
|
183
|
+
// Rule 1 (platform root) — specs/065-manifest-aware-migrations/research.md §R9.
|
|
184
|
+
// `setting_values.sales_channel_id`, `setting_sales_channels` and
|
|
185
|
+
// `setting_group_sales_channels` foreign-key `sales_channels`, and the edge is
|
|
186
|
+
// deliberately not declared: settings is a platform root that every other
|
|
187
|
+
// module (including sales_channels itself) installs on top of, and declaring
|
|
188
|
+
// it would cycle settings → sales_channels → settings.
|
|
189
|
+
//
|
|
190
|
+
// Since feature 072 (T018/T019) all four of those tables are kernel-owned, so
|
|
191
|
+
// the edge no longer crosses a module boundary at all and the acknowledged
|
|
192
|
+
// entry in test/unit/db/acknowledged-fk-edges.ts has been removed. The
|
|
193
|
+
// reasoning is kept because it is why this module declares no edge to
|
|
194
|
+
// `sales_channels`.
|
|
195
|
+
//
|
|
196
|
+
// Feature 072 (T118) — `auth` *is* declared: the admin routes are gated by
|
|
197
|
+
// `requireAdmin` and name the acting admin through `adminAuditActorResolver`,
|
|
198
|
+
// both of which `auth` owns. It closes no cycle (`auth` → `admin_roles` → ∅)
|
|
199
|
+
// and shifts no migration order, because T018 moved this module's tables into
|
|
200
|
+
// the kernel and it ships no migrations of its own.
|
|
201
|
+
dependencies: ['auth'],
|
|
202
|
+
settings,
|
|
203
|
+
// Feature 074 (Constitution XVII), test C2 — functional base, and named by
|
|
204
|
+
// ruling 1. The control that used to be here was one of the nineteen that
|
|
205
|
+
// never accepted a deactivation; making the lock explicit replaces an
|
|
206
|
+
// accidental refusal with a declared one and takes the dead button off the
|
|
207
|
+
// screen.
|
|
208
|
+
//
|
|
209
|
+
// The recoverability argument that used to carry the switch — D-36 moved the
|
|
210
|
+
// activation controls onto the kernel-served `/platform/modules`, T118 made
|
|
211
|
+
// the settings reader kernel-composed — is still true and is why switching
|
|
212
|
+
// this off is survivable. It is not why it should be offered. This module is
|
|
213
|
+
// the configuration surface for every other one, and a module that is off
|
|
214
|
+
// has no editable configuration, so `settings` off means nothing on the
|
|
215
|
+
// platform is configurable: a different product, not a smaller one.
|
|
216
|
+
//
|
|
217
|
+
// `settings.enabled` goes with the control. Left declared it would classify
|
|
218
|
+
// as an ordinary editable boolean that changes nothing, which is the
|
|
219
|
+
// present-but-ignored shape Principle XVII prohibits; the reconciler never
|
|
220
|
+
// deletes a row it stops seeing, so the existing rows are removed by a core
|
|
221
|
+
// data migration instead (feature 074, FR-010a).
|
|
222
|
+
activation: {
|
|
223
|
+
nonDeactivatable: true,
|
|
224
|
+
reason: 'The configuration surface for every other module. A module that is off has no editable ' +
|
|
225
|
+
'configuration, so switching this off would leave nothing on the platform configurable.',
|
|
226
|
+
},
|
|
227
|
+
/**
|
|
228
|
+
* The ten error codes this module owns — feature 090 Phase 3
|
|
229
|
+
* (`specs/090-module-owned-error-codes/contracts/error-code-declaration.md`
|
|
230
|
+
* §1.1). This is where each one's sentence is looked up from: `errors.<CODE>`
|
|
231
|
+
* in this module's own `i18n/{en,pl}.json`.
|
|
232
|
+
*
|
|
233
|
+
* The list is answer-preserving, not a judgement (§6.2 and §6.5), and it was
|
|
234
|
+
* not written by hand: it is the verbatim output of the runbook's step-1
|
|
235
|
+
* derivation over the frozen capture at
|
|
236
|
+
* `backend/test/fixtures/error-code-routing/chain-answers.ts`, which records
|
|
237
|
+
* what the prefix chain in `@endora-commerce/mod-i18n` answered at
|
|
238
|
+
* `49f3c6817`. Re-routing a code to a better owner is
|
|
239
|
+
* `specs/082-error-code-ownership/rulings.md` §9's remaining work and is
|
|
240
|
+
* deliberately not done here.
|
|
241
|
+
*
|
|
242
|
+
* Nine of the ten carry a written sentence in both languages in this
|
|
243
|
+
* package's bundles, and those nine are exactly the `errors.*` keys those
|
|
244
|
+
* bundles hold — so there is no dead sentence in either direction. The tenth,
|
|
245
|
+
* `SETTING_SECRET_KEY_MISSING`, is already a `check-error-translations.ts`
|
|
246
|
+
* `UNTRANSLATED_ERROR_CODES` entry and stays one; declaring it changes
|
|
247
|
+
* nothing about that ledger, which is keyed off the chain.
|
|
248
|
+
*
|
|
249
|
+
* **No shadow reaches this module, and it is the one module that could have
|
|
250
|
+
* cast one.** Trap T1 is about the chain being an ordered `if`, and
|
|
251
|
+
* `SETTING_` is its *first* branch — ahead even of the generic set — so
|
|
252
|
+
* nothing above it can claim a `SETTING_`-prefixed code and every such member
|
|
253
|
+
* of `ERROR_CODES` lands here. Reading the rule's source and reading the
|
|
254
|
+
* chain's answer coincide, which for a migrating author is a conclusion of
|
|
255
|
+
* reading the whole chain and never a premise: the list below still comes off
|
|
256
|
+
* the answer. Nor does this branch take anything from a later one — no
|
|
257
|
+
* `SETTING_`-prefixed code belongs anywhere else under any reading.
|
|
258
|
+
*
|
|
259
|
+
* **Two codes here read like another module's, and both stay** (trap T2).
|
|
260
|
+
* `SETTING_OUT_OF_SCOPE_FOR_CHANNEL` names a sales channel and !1125 recorded
|
|
261
|
+
* it as a code `sales_channels` would look for and not find;
|
|
262
|
+
* `SETTING_SECRET_KEY_MISSING` is raised out of a secret codec that
|
|
263
|
+
* `@endora-commerce/platform`, `credentials` and `ksef` each ship a copy of.
|
|
264
|
+
* Both are refusals about a setting's value, which is the noun the chain
|
|
265
|
+
* follows.
|
|
266
|
+
*
|
|
267
|
+
* **The inverse holds and is the larger half.** This module raises four codes
|
|
268
|
+
* it does not own: `MODULE_ACTIVATION_PROTECTED` (twice) and
|
|
269
|
+
* `MODULE_SETTING_READ_ONLY` on the chain's `MODULE_` branch,
|
|
270
|
+
* `VERSION_CONFLICT` and `INTERNAL` in its generic set — all four route to
|
|
271
|
+
* `core` and are not declared here. And `invoices`'
|
|
272
|
+
* `INVOICE_NUMBER_PATTERN_COLLIDES` is refused *inside this module's write
|
|
273
|
+
* path*: the D-95.2 validator seam has `settings` answer "what would every
|
|
274
|
+
* channel's value be after this write" and the declaring module answer "is
|
|
275
|
+
* that legal", so the throw is `invoices`' own and !1121 declared it there.
|
|
276
|
+
* Routing follows the domain noun, never the thrower.
|
|
277
|
+
*
|
|
278
|
+
* **Three of the ten are raised outside this package**, which is the same
|
|
279
|
+
* rule seen from the other side and is why the raise-site grep is over
|
|
280
|
+
* `packages backend/src` rather than over this module: the platform's
|
|
281
|
+
* activation Command and `audit_logs`' recent-activity Command each raise
|
|
282
|
+
* `SETTING_NOT_REGISTERED` when a module declares a setting the reconciler
|
|
283
|
+
* never created a row for, and `search`'s LLM toggle raises
|
|
284
|
+
* `SETTING_OUT_OF_SCOPE_FOR_CHANNEL`.
|
|
285
|
+
*
|
|
286
|
+
* **Two of the ten are raised by nothing, and they are a kind the register
|
|
287
|
+
* does not yet hold** — `SETTING_CODE_CONFLICT` and
|
|
288
|
+
* `SETTING_BREAKING_CHANGE_REJECTED`.
|
|
289
|
+
* Both rules exist, both are enforced today and both are asserted by
|
|
290
|
+
* `backend/test/unit/settings/manifest-reconciler.test.ts`; they refuse where
|
|
291
|
+
* no envelope reaches. `@endora-commerce/platform`'s settings manifest
|
|
292
|
+
* reconciler throws `SettingCodeConflict` and `BreakingChangeRejected`, plain
|
|
293
|
+
* `Error` subclasses, at boot and on `module:install`, and a setting
|
|
294
|
+
* *definition* has no HTTP door at all — `routes.admin.ts` exposes values and
|
|
295
|
+
* groups and never definitions — so the HTTP code was never wired to the
|
|
296
|
+
* refusal it names. That is none of the four kinds
|
|
297
|
+
* `specs/deferred-defects.md` records: the schema does not pre-empt it, no
|
|
298
|
+
* sibling guard answers differently, no later rule superseded it, and it was
|
|
299
|
+
* not left unbuilt. The rule was built, and the `errors.*` sentence is a
|
|
300
|
+
* second representation of it that no client can receive. Not repaired here —
|
|
301
|
+
* both are declared, because ownership follows the capture and not the raise
|
|
302
|
+
* sites (trap T10), and dropping either reds the progress test as
|
|
303
|
+
* `undeclared`.
|
|
304
|
+
*
|
|
305
|
+
* Both spellings were searched, which trap T12 asks for: `ERROR_CODES.<CODE>`
|
|
306
|
+
* and the bare quoted literal, over `packages`, `backend`, `admin` and
|
|
307
|
+
* `storefront`. The only occurrences of either are the enumeration entry in
|
|
308
|
+
* `@endora-commerce/contracts` and this declaration. What makes the miss
|
|
309
|
+
* legible is that the pattern which would have connected them exists in the
|
|
310
|
+
* same directory and was not applied: `SettingNotRegistered`,
|
|
311
|
+
* `SettingOutOfScopeForChannel` and `SettingValueShapeMismatch` in
|
|
312
|
+
* `kernel/settings/settings.service.ts` each carry
|
|
313
|
+
* `readonly code = 'SETTING_…' as const`, while `BreakingChangeRejected`
|
|
314
|
+
* carries no `code` and `SettingCodeConflict`'s `code` is the *setting's*
|
|
315
|
+
* code, not an error code — a name collision that reads like the link and is
|
|
316
|
+
* not one.
|
|
317
|
+
*
|
|
318
|
+
* No `tokens`, derived rather than assumed. `refusalToken`
|
|
319
|
+
* (`packages/platform/src/http/error-envelope.ts`) reads `details.code` and
|
|
320
|
+
* nothing else; all seventeen raises of these ten codes across `packages` and
|
|
321
|
+
* `backend/src` were read, fifteen pass no fourth argument at all, and the
|
|
322
|
+
* two that do — `search`'s LLM toggle and this module's own `valueType`
|
|
323
|
+
* refusal — pass the Zod-style `Array<{path, issue}>`, which `refusalToken`
|
|
324
|
+
* returns `null` for by construction. The runbook's §5 raise-site scan
|
|
325
|
+
* attributes the tree's ten token-carrying codes over 41 sites to `core`,
|
|
326
|
+
* `invoices` and `carts` and names none of these, and the bundles hold no
|
|
327
|
+
* `errors.<CODE>.<token>` key in the other direction.
|
|
328
|
+
*/
|
|
329
|
+
errorCodes: [
|
|
330
|
+
{ code: 'SETTING_BREAKING_CHANGE_REJECTED' },
|
|
331
|
+
{ code: 'SETTING_CODE_CONFLICT' },
|
|
332
|
+
{ code: 'SETTING_EMPTY_SUBSET' },
|
|
333
|
+
{ code: 'SETTING_GROUP_CODE_CONFLICT' },
|
|
334
|
+
{ code: 'SETTING_GROUP_NOT_FOUND' },
|
|
335
|
+
{ code: 'SETTING_GROUP_PROTECTED' },
|
|
336
|
+
{ code: 'SETTING_NOT_REGISTERED' },
|
|
337
|
+
{ code: 'SETTING_OUT_OF_SCOPE_FOR_CHANNEL' },
|
|
338
|
+
{ code: 'SETTING_SECRET_KEY_MISSING' },
|
|
339
|
+
{ code: 'SETTING_VALUE_SHAPE_MISMATCH' },
|
|
340
|
+
],
|
|
341
|
+
i18n: { bundlesDir: 'i18n' },
|
|
342
|
+
docs: { dir: 'docs' },
|
|
343
|
+
permissions: [
|
|
344
|
+
{ code: 'settings:read', label: 'View settings' },
|
|
345
|
+
{ code: 'settings:write', label: 'Edit settings' },
|
|
346
|
+
],
|
|
347
|
+
actions: [
|
|
348
|
+
{
|
|
349
|
+
id: 'open-settings',
|
|
350
|
+
labelKey: 'actions.openSettings.label',
|
|
351
|
+
descriptionKey: 'actions.openSettings.description',
|
|
352
|
+
icon: 'Settings',
|
|
353
|
+
targetRoute: '/settings',
|
|
354
|
+
// The only one of the 53 shipped actions that declared no code at all
|
|
355
|
+
// (issue #232), while `/api/v1/admin/settings` is
|
|
356
|
+
// `requireAdmin('settings:read')` — so the palette offered the screen to
|
|
357
|
+
// every role and every role without the code collected a 403 on arrival.
|
|
358
|
+
requiredPermission: 'settings:read',
|
|
359
|
+
keywords: ['settings', 'preferences', 'config', 'ustawienia'],
|
|
360
|
+
weight: 250,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
});
|
|
364
|
+
/** Legacy export retained for backward compatibility. */
|
|
365
|
+
export const settingsManifest = settings;
|
|
366
|
+
/**
|
|
367
|
+
* The operator command this module declares — feature 080, T042b / D-160.9.
|
|
368
|
+
*
|
|
369
|
+
* It was `scripts/cache-clear.ts`, which opened its own Redis connection and
|
|
370
|
+
* built its own `CacheAdminService`. It flushes through the composition's now.
|
|
371
|
+
*/
|
|
372
|
+
export const cliCommands = [
|
|
373
|
+
{
|
|
374
|
+
name: 'cache-clear',
|
|
375
|
+
summary: 'Flush selected (or all) Redis cache namespaces.',
|
|
376
|
+
run: async (context) => (await import('./backend/cli/cache-clear.js')).cacheClear(context),
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Settings
|
|
3
|
+
sidebar_position: 1
|
|
4
|
+
description: Manifest-driven, per-sales-channel platform configuration with cached read API
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Settings
|
|
8
|
+
|
|
9
|
+
The Settings module owns platform-wide configuration. Other modules contribute
|
|
10
|
+
their own setting groups and individual settings through a typed manifest;
|
|
11
|
+
platform administrators tune values per sales channel from the Admin UI; any
|
|
12
|
+
backend module reads values through one well-known service.
|
|
13
|
+
|
|
14
|
+
## Concepts
|
|
15
|
+
|
|
16
|
+
- **Setting** — a single tunable knob. Carries a name, a globally-unique
|
|
17
|
+
machine code, a value type (`string` / `number` / `boolean` / `json` /
|
|
18
|
+
`string_list`), a manifest-supplied default value, and a sales-channel
|
|
19
|
+
scope (empty scope means "applies to every channel").
|
|
20
|
+
- **Setting Value** — an admin-chosen value for a `(setting, sales_channel)`
|
|
21
|
+
pair. Replaces the manifest default for that channel. Resolution order is
|
|
22
|
+
always: per-channel value → manifest default.
|
|
23
|
+
- **Setting Group** — a logical section under which related settings appear in
|
|
24
|
+
the Admin UI. The built-in `general` group is system-protected. Deleting any
|
|
25
|
+
other group reassigns its settings to `general` and preserves their values.
|
|
26
|
+
|
|
27
|
+
## For module authors — declaring your settings
|
|
28
|
+
|
|
29
|
+
Each module that wants to register settings or groups exports a sibling
|
|
30
|
+
`manifest.ts` file using the helper from `@endora-commerce/contracts`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// packages/modules/<your_module>/src/manifest.ts
|
|
34
|
+
import { defineModuleSettingsManifest } from '@endora-commerce/contracts';
|
|
35
|
+
|
|
36
|
+
export const settingsManifest = defineModuleSettingsManifest({
|
|
37
|
+
moduleCode: 'your_module',
|
|
38
|
+
groups: [{ code: 'your_section', name: 'Your section' }],
|
|
39
|
+
settings: [
|
|
40
|
+
{
|
|
41
|
+
code: 'your_module.base_url',
|
|
42
|
+
name: 'Base URL',
|
|
43
|
+
groupCode: 'your_section', // optional — defaults to 'general'
|
|
44
|
+
valueType: 'string',
|
|
45
|
+
defaultValue: 'https://default.example',
|
|
46
|
+
// salesChannelCodes: ['main', 'wholesale'] // optional — empty = all
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Append your manifest to the array in `backend/src/composition.ts` — the
|
|
53
|
+
boot-time reconciler walks every entry and inserts any missing rows
|
|
54
|
+
idempotently. Re-running is always safe; admin-chosen values are never
|
|
55
|
+
overwritten.
|
|
56
|
+
|
|
57
|
+
### Reconciler guarantees
|
|
58
|
+
|
|
59
|
+
| Behavior | Outcome |
|
|
60
|
+
|----------|---------|
|
|
61
|
+
| First-time apply | New groups + settings inserted; `owner_module` set to your module code. |
|
|
62
|
+
| Re-apply (no changes) | No-op. |
|
|
63
|
+
| Re-apply (added entry) | Only new entries are inserted. |
|
|
64
|
+
| Re-apply (renamed `name`/`description`) | Updated in place. |
|
|
65
|
+
| Re-apply (changed `valueType` or `defaultValue`) | Rejected without `--force` to keep existing per-channel values valid. |
|
|
66
|
+
| Re-apply (entry removed from manifest) | Boot sync ignores the removal — orphan rows are logged but not deleted. Only `modules:uninstall` is destructive. |
|
|
67
|
+
| Setting code conflicts with another module | Reconciliation aborts with a clear error. |
|
|
68
|
+
|
|
69
|
+
## For platform admins — editing values
|
|
70
|
+
|
|
71
|
+
Open `Admin → Operations → Settings`. Settings are grouped by their owning
|
|
72
|
+
section. Selecting one opens an editor on the right; you can:
|
|
73
|
+
|
|
74
|
+
- Apply a value to **every sales channel in scope** in one click.
|
|
75
|
+
- Apply a value to a **chosen subset** of channels (the editor enforces the
|
|
76
|
+
"at least one channel" rule).
|
|
77
|
+
- **Reset** the per-channel values back to the manifest default.
|
|
78
|
+
|
|
79
|
+
Concurrent edits are detected via an `expectedVersion` (ISO timestamp). If
|
|
80
|
+
someone else has changed the setting since you opened it, the save returns
|
|
81
|
+
`409 VERSION_CONFLICT` and a banner asks you to refresh and retry — no silent
|
|
82
|
+
overwrites.
|
|
83
|
+
|
|
84
|
+
Setting groups are managed under `Admin → Operations → Setting groups`.
|
|
85
|
+
The `general` group is system-protected; the platform refuses to delete it.
|
|
86
|
+
Deleting any other group reassigns its settings to `general` and preserves
|
|
87
|
+
their per-channel values.
|
|
88
|
+
|
|
89
|
+
Every value change and group mutation lands an `audit_log_entries` row with
|
|
90
|
+
the actor, action, target, and new value.
|
|
91
|
+
|
|
92
|
+
## For module consumers — reading values
|
|
93
|
+
|
|
94
|
+
Other modules inject `SettingsService` from the composition root and read
|
|
95
|
+
through a single API. The result is always either the admin-chosen value
|
|
96
|
+
for the requested channel or the manifest default.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { z } from 'zod';
|
|
100
|
+
|
|
101
|
+
// In your module's plugin:
|
|
102
|
+
const baseUrl = await settingsService.get(
|
|
103
|
+
'your_module.base_url',
|
|
104
|
+
request.salesChannelId,
|
|
105
|
+
z.string().url(),
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Errors
|
|
110
|
+
|
|
111
|
+
| Thrown | When |
|
|
112
|
+
|--------|------|
|
|
113
|
+
| `SettingNotRegistered` | The code has no row in `settings`. The caller has a typo or the module that should declare it has not been installed. |
|
|
114
|
+
| `SettingOutOfScopeForChannel` | The setting was registered with an explicit channel scope and the requested channel is not in that scope. Indicates a programmer error: the consuming module should not be reading this setting in this context. |
|
|
115
|
+
| `SettingValueShapeMismatch` | The stored value passed Zod validation at write time but failed the caller's schema (e.g. an admin set the value via a manifest with a wider type). Surfaced as a fail-fast misconfiguration. |
|
|
116
|
+
|
|
117
|
+
### Performance
|
|
118
|
+
|
|
119
|
+
The universal getter is cheap enough to call freely from request paths.
|
|
120
|
+
Resolution order:
|
|
121
|
+
|
|
122
|
+
1. Per-process LRU (1024 entries, entries older than 30 s ignored).
|
|
123
|
+
2. Redis (`settings:v1:<code>:<channelId>`, TTL 1h).
|
|
124
|
+
3. Postgres (one keyed lookup against `(setting_id, sales_channel_id)`).
|
|
125
|
+
|
|
126
|
+
Cache invalidation hangs off the EventBus events that the admin service
|
|
127
|
+
emits on every value or group mutation. The EventBus is in-process, so the
|
|
128
|
+
writing process picks the change up immediately, and every other process
|
|
129
|
+
picks it up on its next read past the 30 s window — invalidation dropped the
|
|
130
|
+
shared Redis entry, and the local window is measured from when the value was
|
|
131
|
+
loaded rather than from the last read, so even a setting read on every request
|
|
132
|
+
ages out.
|
|
133
|
+
|
|
134
|
+
That window is the bound on how long a missed invalidation can be visible. It
|
|
135
|
+
is also why the operator-facing "clear cache" page is a diagnostic tool rather
|
|
136
|
+
than a repair: it drops both layers in the process that serves it and the
|
|
137
|
+
shared entries for everyone, and every other process converges within the same
|
|
138
|
+
30 s.
|
|
139
|
+
|
|
140
|
+
## CLI
|
|
141
|
+
|
|
142
|
+
Two scripts ship with the backend:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Idempotently reconcile a module's manifest into the database.
|
|
146
|
+
pnpm --filter backend run modules:install <module-code> [--force] [--dry-run]
|
|
147
|
+
|
|
148
|
+
# Remove a module's settings + groups. The flag is required — there is no
|
|
149
|
+
# implicit default. --remove-settings deletes everything owned by the module
|
|
150
|
+
# (cascade-deletes per-channel values); --preserve-settings keeps everything
|
|
151
|
+
# in place so a future re-install picks the rows up unchanged.
|
|
152
|
+
pnpm --filter backend run modules:uninstall <module-code> \
|
|
153
|
+
(--remove-settings | --preserve-settings)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Both commands write `audit_log_entries` rows. `modules:install` exit codes:
|
|
157
|
+
`0` success, `64` misuse, `65` invalid manifest, `66` conflict, `70` internal
|
|
158
|
+
error.
|
|
159
|
+
|
|
160
|
+
## Database
|
|
161
|
+
|
|
162
|
+
Five tables introduced by migration `024_settings_init.ts`:
|
|
163
|
+
|
|
164
|
+
- `setting_groups` (with `is_system_protected` for the built-in `general`)
|
|
165
|
+
- `settings` (FK → `setting_groups`, value-type enum, `jsonb` default)
|
|
166
|
+
- `setting_values` (per-`(setting, sales_channel)` admin override; UNIQUE)
|
|
167
|
+
- `setting_group_sales_channels` (M:N scope)
|
|
168
|
+
- `setting_sales_channels` (M:N scope; empty = all channels)
|
|
169
|
+
|
|
170
|
+
`setting_values.sales_channel_id` cascade-deletes when the channel is
|
|
171
|
+
removed; remaining bindings on the same setting are preserved.
|