@hartl-services/medusa-base 0.1.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/.medusa/server/src/admin/index.js +267 -0
- package/.medusa/server/src/admin/index.mjs +266 -0
- package/.medusa/server/src/api/admin/plugin-features/route.js +20 -0
- package/.medusa/server/src/api/middlewares.js +25 -0
- package/.medusa/server/src/api/plugin-features-http.js +20 -0
- package/.medusa/server/src/api/store/plugin-features/[plugin_key]/route.js +13 -0
- package/.medusa/server/src/api/store/plugin-features/route.js +13 -0
- package/.medusa/server/src/api/wire-response.js +10 -0
- package/.medusa/server/src/modules/plugin-features/index.js +14 -0
- package/.medusa/server/src/modules/plugin-features/migrations/Migration20260928155256.js +16 -0
- package/.medusa/server/src/modules/plugin-features/models/plugin-feature-state.js +29 -0
- package/.medusa/server/src/modules/plugin-features/service.js +141 -0
- package/.medusa/server/src/public/constants.js +6 -0
- package/.medusa/server/src/public/descriptor.js +54 -0
- package/.medusa/server/src/public/effective-features.js +74 -0
- package/.medusa/server/src/public/enforcement.js +51 -0
- package/.medusa/server/src/public/index.js +13 -0
- package/.medusa/server/src/public/types.js +3 -0
- package/.medusa/server/src/utils/discover-plugin-features.js +51 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/public/constants.d.ts +2 -0
- package/dist/public/constants.js +5 -0
- package/dist/public/descriptor.d.ts +11 -0
- package/dist/public/descriptor.js +53 -0
- package/dist/public/effective-features.d.ts +28 -0
- package/dist/public/effective-features.js +73 -0
- package/dist/public/enforcement.d.ts +13 -0
- package/dist/public/enforcement.js +50 -0
- package/dist/public/index.d.ts +5 -0
- package/dist/public/index.js +12 -0
- package/dist/public/types.d.ts +29 -0
- package/dist/public/types.js +2 -0
- package/package.json +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Medusa Base
|
|
2
|
+
|
|
3
|
+
Medusa v2.19 base plugin for Hartl Services shops. Every shop installs it. It
|
|
4
|
+
provides per-plugin feature toggles: a generic Admin page under
|
|
5
|
+
**Settings → Hartl Services Plugins**, a Store endpoint for storefronts and
|
|
6
|
+
helpers that plugins use to enforce their toggles.
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
- Discovers every registered module whose service declares `pluginFeatures`
|
|
11
|
+
(see below). No plugin has to register itself anywhere else.
|
|
12
|
+
- Stores one row per plugin in `plugin_feature_state` (module
|
|
13
|
+
`plugin_features`): the plugin switch, the configured feature values and a
|
|
14
|
+
`version` for optimistic concurrency.
|
|
15
|
+
- Serves the effective values to storefronts and the configured plus effective
|
|
16
|
+
values to the Admin.
|
|
17
|
+
- Exports `definePluginFeatures`, `requirePluginFeature` and
|
|
18
|
+
`isPluginFeatureEnabled` from `@hartl-services/medusa-base` for plugins.
|
|
19
|
+
|
|
20
|
+
## Installation in a shop
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @hartl-services/medusa-base @hartl-services/medusa-food-supplements-contracts
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// medusa-config.ts
|
|
28
|
+
plugins: [
|
|
29
|
+
// First in the list: other plugins read their toggles through it.
|
|
30
|
+
{
|
|
31
|
+
resolve: "@hartl-services/medusa-base",
|
|
32
|
+
options: {},
|
|
33
|
+
},
|
|
34
|
+
// … all other plugins
|
|
35
|
+
]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The plugin has no options. Run `medusa db:migrate` after installing it.
|
|
39
|
+
|
|
40
|
+
Peer dependencies are the host's Medusa 2.19 packages (`@medusajs/framework`,
|
|
41
|
+
`@medusajs/medusa`, `@medusajs/admin-sdk`, `@medusajs/js-sdk`, `@medusajs/ui`)
|
|
42
|
+
plus React, React Router and TanStack Query in the versions the Medusa 2.19
|
|
43
|
+
dashboard uses.
|
|
44
|
+
|
|
45
|
+
## HTTP endpoints
|
|
46
|
+
|
|
47
|
+
| Method | Path | Auth | Response |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `GET` | `/store/plugin-features` | publishable key | `StorePluginFeaturesListResponse` |
|
|
50
|
+
| `GET` | `/store/plugin-features/:plugin_key` | publishable key | `StorePluginFeaturesResponse`, 404 for an unknown plugin |
|
|
51
|
+
| `GET` | `/admin/plugin-features` | Admin user | `AdminPluginFeaturesListResponse` |
|
|
52
|
+
| `POST` | `/admin/plugin-features` | Admin user | `AdminPluginFeaturesResponse` |
|
|
53
|
+
|
|
54
|
+
Every authenticated Admin user may change the toggles (no RBAC by design).
|
|
55
|
+
`POST` takes `{ plugin_key, expected_version, enabled, features }`; `features`
|
|
56
|
+
holds configured values for declared keys, missing keys keep their default and
|
|
57
|
+
an unknown key is 400. An unknown `plugin_key` is 404, a stale
|
|
58
|
+
`expected_version` is 409.
|
|
59
|
+
|
|
60
|
+
Store values are effective values only; a storefront uses them to hide
|
|
61
|
+
features. DTOs, schemas and paths come from the Contracts package:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import type { StorePluginFeaturesListResponse } from "@hartl-services/medusa-food-supplements-contracts/types/plugin-features-store"
|
|
65
|
+
import { pluginFeaturesStorePaths } from "@hartl-services/medusa-food-supplements-contracts/paths/plugin-features"
|
|
66
|
+
|
|
67
|
+
const { plugins } = await sdk.client.fetch<StorePluginFeaturesListResponse>(
|
|
68
|
+
pluginFeaturesStorePaths.get.list,
|
|
69
|
+
{ method: "GET" }
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Declaring features in a plugin
|
|
74
|
+
|
|
75
|
+
A plugin depends on `@hartl-services/medusa-base` (peer dependency), declares
|
|
76
|
+
its features once and exposes the declaration as `pluginFeatures` on its
|
|
77
|
+
module service:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// src/plugin-features.ts
|
|
81
|
+
import { definePluginFeatures } from "@hartl-services/medusa-base"
|
|
82
|
+
|
|
83
|
+
export const demoPluginFeatures = definePluginFeatures({
|
|
84
|
+
plugin_key: "demo",
|
|
85
|
+
label: "Demo-Plugin",
|
|
86
|
+
// default_enabled: true, // plugin switch default
|
|
87
|
+
features: [
|
|
88
|
+
{ key: "coupons", label: "Gutscheine", default_enabled: true },
|
|
89
|
+
{
|
|
90
|
+
key: "coupons_self_service",
|
|
91
|
+
label: "Gutscheine selbst verwalten",
|
|
92
|
+
default_enabled: false,
|
|
93
|
+
parent_key: "coupons",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// src/modules/demo/service.ts
|
|
99
|
+
class DemoModuleService extends MedusaService({ /* models */ }) {
|
|
100
|
+
readonly pluginFeatures = demoPluginFeatures
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Keys match `^[a-z][a-z0-9_]{0,62}$`, feature keys are unique, a `parent_key`
|
|
105
|
+
must exist and only one nesting level is allowed. `definePluginFeatures`
|
|
106
|
+
throws at import time otherwise.
|
|
107
|
+
|
|
108
|
+
### Gating routes
|
|
109
|
+
|
|
110
|
+
Put gate entries at the start of the plugin's `routes` array. An entry without
|
|
111
|
+
`methods` is a prefix match and runs before every method-specific middleware:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// src/api/middlewares.ts
|
|
115
|
+
import { requirePluginFeature } from "@hartl-services/medusa-base"
|
|
116
|
+
|
|
117
|
+
export default defineMiddlewares({
|
|
118
|
+
routes: [
|
|
119
|
+
{ matcher: "/store/demo", middlewares: [requirePluginFeature(demoPluginFeatures)] },
|
|
120
|
+
{
|
|
121
|
+
matcher: "/store/demo/coupons",
|
|
122
|
+
middlewares: [requirePluginFeature(demoPluginFeatures, "coupons")],
|
|
123
|
+
},
|
|
124
|
+
// … existing entries unchanged
|
|
125
|
+
],
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A disabled plugin or feature answers 404. With several keys, all must be
|
|
130
|
+
enabled. The toggles are read once per request.
|
|
131
|
+
|
|
132
|
+
### Gating subscribers and jobs
|
|
133
|
+
|
|
134
|
+
Return early:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { isPluginFeatureEnabled } from "@hartl-services/medusa-base"
|
|
138
|
+
|
|
139
|
+
if (!(await isPluginFeatureEnabled(container, demoPluginFeatures, "coupons"))) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
A read failure throws, so the subscriber or job is retried. A missing base
|
|
145
|
+
plugin fails with `UNEXPECTED_STATE` ("@hartl-services/medusa-base ist nicht in
|
|
146
|
+
medusa-config registriert.").
|
|
147
|
+
|
|
148
|
+
### Never on the cart path
|
|
149
|
+
|
|
150
|
+
Code on the cart and checkout hot path (cart middlewares, cart workflow hooks,
|
|
151
|
+
`orderCreated` handling, cart subscribers) must never read toggles. It stays
|
|
152
|
+
fail-safe and independent of the database state of this plugin. Toggles
|
|
153
|
+
control visibility and reachability of features, not the integrity of
|
|
154
|
+
existing data.
|
|
155
|
+
|
|
156
|
+
## Semantics
|
|
157
|
+
|
|
158
|
+
- No stored row: every value is its declared default, `version` is 0.
|
|
159
|
+
- `effective(feature) = plugin enabled && configured(feature) && effective(parent)`.
|
|
160
|
+
Turning a parent off turns its children off; their configured values stay.
|
|
161
|
+
Turning the plugin off turns every feature off.
|
|
162
|
+
- Stored values for keys a plugin no longer declares are ignored; a feature
|
|
163
|
+
added later uses its default until it is saved.
|
|
164
|
+
- Updates are optimistic: `expected_version` 0 creates the row, any other
|
|
165
|
+
value updates exactly that version. A concurrent change answers 409; the
|
|
166
|
+
Admin page reloads and shows a notice.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PluginFeatureDescriptor } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Checks key format, unique feature keys, existing parents and the single
|
|
4
|
+
* allowed nesting level. Throws `INVALID_DATA`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validatePluginFeatureDescriptor(descriptor: PluginFeatureDescriptor): void;
|
|
7
|
+
/**
|
|
8
|
+
* Declares a plugin's feature switches. Validates at import time so a broken
|
|
9
|
+
* declaration fails the boot instead of a request.
|
|
10
|
+
*/
|
|
11
|
+
export declare function definePluginFeatures<const K extends string>(descriptor: PluginFeatureDescriptor<K>): PluginFeatureDescriptor<K>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validatePluginFeatureDescriptor = validatePluginFeatureDescriptor;
|
|
4
|
+
exports.definePluginFeatures = definePluginFeatures;
|
|
5
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
6
|
+
const KEY_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
|
|
7
|
+
const invalid = (pluginKey, message) => new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Ungültige Funktionsschalter für Plugin "${pluginKey}": ${message}`);
|
|
8
|
+
/**
|
|
9
|
+
* Checks key format, unique feature keys, existing parents and the single
|
|
10
|
+
* allowed nesting level. Throws `INVALID_DATA`.
|
|
11
|
+
*/
|
|
12
|
+
function validatePluginFeatureDescriptor(descriptor) {
|
|
13
|
+
const pluginKey = String(descriptor.plugin_key);
|
|
14
|
+
if (!KEY_PATTERN.test(pluginKey)) {
|
|
15
|
+
throw invalid(pluginKey, "plugin_key hat ein ungültiges Format.");
|
|
16
|
+
}
|
|
17
|
+
if (typeof descriptor.label !== "string" || !descriptor.label.trim()) {
|
|
18
|
+
throw invalid(pluginKey, "label fehlt.");
|
|
19
|
+
}
|
|
20
|
+
const byKey = new Map();
|
|
21
|
+
for (const feature of descriptor.features) {
|
|
22
|
+
if (!KEY_PATTERN.test(feature.key)) {
|
|
23
|
+
throw invalid(pluginKey, `Feature-Key "${feature.key}" hat ein ungültiges Format.`);
|
|
24
|
+
}
|
|
25
|
+
if (byKey.has(feature.key)) {
|
|
26
|
+
throw invalid(pluginKey, `Feature-Key "${feature.key}" ist doppelt.`);
|
|
27
|
+
}
|
|
28
|
+
if (typeof feature.label !== "string" || !feature.label.trim()) {
|
|
29
|
+
throw invalid(pluginKey, `Feature "${feature.key}" hat kein label.`);
|
|
30
|
+
}
|
|
31
|
+
byKey.set(feature.key, feature);
|
|
32
|
+
}
|
|
33
|
+
for (const feature of descriptor.features) {
|
|
34
|
+
if (feature.parent_key === undefined) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const parent = byKey.get(feature.parent_key);
|
|
38
|
+
if (!parent || parent.key === feature.key) {
|
|
39
|
+
throw invalid(pluginKey, `Parent "${feature.parent_key}" von Feature "${feature.key}" existiert nicht.`);
|
|
40
|
+
}
|
|
41
|
+
if (parent.parent_key !== undefined) {
|
|
42
|
+
throw invalid(pluginKey, `Feature "${feature.key}" ist zu tief verschachtelt; erlaubt ist eine Ebene.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Declares a plugin's feature switches. Validates at import time so a broken
|
|
48
|
+
* declaration fails the boot instead of a request.
|
|
49
|
+
*/
|
|
50
|
+
function definePluginFeatures(descriptor) {
|
|
51
|
+
validatePluginFeatureDescriptor(descriptor);
|
|
52
|
+
return descriptor;
|
|
53
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { EffectivePluginFeatures, PluginFeatureDefinition, PluginFeatureDescriptor, StoredPluginFeatures } from "./types";
|
|
2
|
+
export type PluginFeatureState = {
|
|
3
|
+
definition: PluginFeatureDefinition;
|
|
4
|
+
/** Configured value: stored or default. */
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
/** After the plugin switch and the parent rule. */
|
|
7
|
+
effective_enabled: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type PluginFeatureStates = {
|
|
10
|
+
/** Configured plugin switch: stored or default. */
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
features: PluginFeatureState[];
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Configured and effective values of every declared feature.
|
|
16
|
+
* `effective(f) = pluginEnabled && configured(f) && effective(parent(f))`.
|
|
17
|
+
* Stored keys the descriptor no longer declares are ignored; declared keys
|
|
18
|
+
* without a stored value fall back to their default.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolvePluginFeatureStates(descriptor: PluginFeatureDescriptor, stored: StoredPluginFeatures | null): PluginFeatureStates;
|
|
21
|
+
/** Pure rule shared by service and tests. `stored === null` means no row. */
|
|
22
|
+
export declare function resolveEffectivePluginFeatures(descriptor: PluginFeatureDescriptor, stored: StoredPluginFeatures | null): EffectivePluginFeatures;
|
|
23
|
+
/**
|
|
24
|
+
* Rejects configured values for keys the descriptor does not declare and
|
|
25
|
+
* returns the values to persist (declared keys only; missing keys keep their
|
|
26
|
+
* default).
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateFeatureUpdate(descriptor: PluginFeatureDescriptor, features: Record<string, boolean>): Record<string, boolean>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolvePluginFeatureStates = resolvePluginFeatureStates;
|
|
4
|
+
exports.resolveEffectivePluginFeatures = resolveEffectivePluginFeatures;
|
|
5
|
+
exports.validateFeatureUpdate = validateFeatureUpdate;
|
|
6
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
7
|
+
/**
|
|
8
|
+
* Configured and effective values of every declared feature.
|
|
9
|
+
* `effective(f) = pluginEnabled && configured(f) && effective(parent(f))`.
|
|
10
|
+
* Stored keys the descriptor no longer declares are ignored; declared keys
|
|
11
|
+
* without a stored value fall back to their default.
|
|
12
|
+
*/
|
|
13
|
+
function resolvePluginFeatureStates(descriptor, stored) {
|
|
14
|
+
const pluginEnabled = stored
|
|
15
|
+
? stored.enabled
|
|
16
|
+
: descriptor.default_enabled ?? true;
|
|
17
|
+
const configured = new Map();
|
|
18
|
+
for (const feature of descriptor.features) {
|
|
19
|
+
const value = stored?.features[feature.key];
|
|
20
|
+
configured.set(feature.key, typeof value === "boolean" ? value : feature.default_enabled);
|
|
21
|
+
}
|
|
22
|
+
const effective = (feature) => {
|
|
23
|
+
if (!pluginEnabled || configured.get(feature.key) !== true) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (feature.parent_key === undefined) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
// Validated descriptors allow one nesting level, so the parent is a root.
|
|
30
|
+
return configured.get(feature.parent_key) === true;
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
enabled: pluginEnabled,
|
|
34
|
+
features: descriptor.features.map((definition) => ({
|
|
35
|
+
definition,
|
|
36
|
+
enabled: configured.get(definition.key) === true,
|
|
37
|
+
effective_enabled: effective(definition),
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Pure rule shared by service and tests. `stored === null` means no row. */
|
|
42
|
+
function resolveEffectivePluginFeatures(descriptor, stored) {
|
|
43
|
+
const states = resolvePluginFeatureStates(descriptor, stored);
|
|
44
|
+
const features = {};
|
|
45
|
+
for (const state of states.features) {
|
|
46
|
+
features[state.definition.key] = state.effective_enabled;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
plugin_key: descriptor.plugin_key,
|
|
50
|
+
enabled: states.enabled,
|
|
51
|
+
features,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Rejects configured values for keys the descriptor does not declare and
|
|
56
|
+
* returns the values to persist (declared keys only; missing keys keep their
|
|
57
|
+
* default).
|
|
58
|
+
*/
|
|
59
|
+
function validateFeatureUpdate(descriptor, features) {
|
|
60
|
+
const declared = new Set(descriptor.features.map((feature) => feature.key));
|
|
61
|
+
const unknown = Object.keys(features).filter((key) => !declared.has(key));
|
|
62
|
+
if (unknown.length) {
|
|
63
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Unbekannte Funktionsschalter für Plugin "${descriptor.plugin_key}": ${unknown.join(", ")}.`);
|
|
64
|
+
}
|
|
65
|
+
const result = {};
|
|
66
|
+
for (const feature of descriptor.features) {
|
|
67
|
+
const value = features[feature.key];
|
|
68
|
+
if (typeof value === "boolean") {
|
|
69
|
+
result[feature.key] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MedusaRequestHandler } from "@medusajs/framework/http";
|
|
2
|
+
import type { MedusaContainer } from "@medusajs/framework/types";
|
|
3
|
+
import type { PluginFeatureDescriptor } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* For subscribers, jobs and workflow steps. No keys = plugin switch only;
|
|
6
|
+
* several keys = all must be enabled. Read failures propagate.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isPluginFeatureEnabled<K extends string>(container: MedusaContainer, descriptor: PluginFeatureDescriptor<K>, ...keys: K[]): Promise<boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* Middleware for `defineMiddlewares`. A disabled plugin or feature answers
|
|
11
|
+
* with `NOT_FOUND` (404); all given keys must be enabled.
|
|
12
|
+
*/
|
|
13
|
+
export declare function requirePluginFeature<K extends string>(descriptor: PluginFeatureDescriptor<K>, ...keys: K[]): MedusaRequestHandler;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPluginFeatureEnabled = isPluginFeatureEnabled;
|
|
4
|
+
exports.requirePluginFeature = requirePluginFeature;
|
|
5
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
const resolveReader = (container) => {
|
|
8
|
+
if (!container.hasRegistration(constants_1.PLUGIN_FEATURES_MODULE)) {
|
|
9
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, "@hartl-services/medusa-base ist nicht in medusa-config registriert.");
|
|
10
|
+
}
|
|
11
|
+
return container.resolve(constants_1.PLUGIN_FEATURES_MODULE);
|
|
12
|
+
};
|
|
13
|
+
const allEnabled = (effective, keys) => effective.enabled && keys.every((key) => effective.features[key] === true);
|
|
14
|
+
/**
|
|
15
|
+
* For subscribers, jobs and workflow steps. No keys = plugin switch only;
|
|
16
|
+
* several keys = all must be enabled. Read failures propagate.
|
|
17
|
+
*/
|
|
18
|
+
async function isPluginFeatureEnabled(container, descriptor, ...keys) {
|
|
19
|
+
const effective = await resolveReader(container).retrieveEffectiveFeatures(descriptor);
|
|
20
|
+
return allEnabled(effective, keys);
|
|
21
|
+
}
|
|
22
|
+
// Keyed by the request object, so one request reads each plugin's toggles once
|
|
23
|
+
// even when several gate middlewares match it.
|
|
24
|
+
const requestMemo = new WeakMap();
|
|
25
|
+
const effectiveForRequest = (req, descriptor) => {
|
|
26
|
+
let perRequest = requestMemo.get(req);
|
|
27
|
+
if (!perRequest) {
|
|
28
|
+
perRequest = new Map();
|
|
29
|
+
requestMemo.set(req, perRequest);
|
|
30
|
+
}
|
|
31
|
+
let pending = perRequest.get(descriptor.plugin_key);
|
|
32
|
+
if (!pending) {
|
|
33
|
+
pending = resolveReader(req.scope).retrieveEffectiveFeatures(descriptor);
|
|
34
|
+
perRequest.set(descriptor.plugin_key, pending);
|
|
35
|
+
}
|
|
36
|
+
return pending;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Middleware for `defineMiddlewares`. A disabled plugin or feature answers
|
|
40
|
+
* with `NOT_FOUND` (404); all given keys must be enabled.
|
|
41
|
+
*/
|
|
42
|
+
function requirePluginFeature(descriptor, ...keys) {
|
|
43
|
+
return async (req, _res, next) => {
|
|
44
|
+
const effective = await effectiveForRequest(req, descriptor);
|
|
45
|
+
if (!allEnabled(effective, keys)) {
|
|
46
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, "Diese Funktion ist in diesem Shop nicht aktiviert.");
|
|
47
|
+
}
|
|
48
|
+
next();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { PLUGIN_FEATURES_MODULE } from "./constants";
|
|
2
|
+
export { definePluginFeatures } from "./descriptor";
|
|
3
|
+
export { resolveEffectivePluginFeatures } from "./effective-features";
|
|
4
|
+
export { isPluginFeatureEnabled, requirePluginFeature } from "./enforcement";
|
|
5
|
+
export type { EffectivePluginFeatures, PluginFeatureDefinition, PluginFeatureDescriptor, PluginFeatureProvider, } from "./types";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requirePluginFeature = exports.isPluginFeatureEnabled = exports.resolveEffectivePluginFeatures = exports.definePluginFeatures = exports.PLUGIN_FEATURES_MODULE = void 0;
|
|
4
|
+
var constants_1 = require("./constants");
|
|
5
|
+
Object.defineProperty(exports, "PLUGIN_FEATURES_MODULE", { enumerable: true, get: function () { return constants_1.PLUGIN_FEATURES_MODULE; } });
|
|
6
|
+
var descriptor_1 = require("./descriptor");
|
|
7
|
+
Object.defineProperty(exports, "definePluginFeatures", { enumerable: true, get: function () { return descriptor_1.definePluginFeatures; } });
|
|
8
|
+
var effective_features_1 = require("./effective-features");
|
|
9
|
+
Object.defineProperty(exports, "resolveEffectivePluginFeatures", { enumerable: true, get: function () { return effective_features_1.resolveEffectivePluginFeatures; } });
|
|
10
|
+
var enforcement_1 = require("./enforcement");
|
|
11
|
+
Object.defineProperty(exports, "isPluginFeatureEnabled", { enumerable: true, get: function () { return enforcement_1.isPluginFeatureEnabled; } });
|
|
12
|
+
Object.defineProperty(exports, "requirePluginFeature", { enumerable: true, get: function () { return enforcement_1.requirePluginFeature; } });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type PluginFeatureDefinition<K extends string = string> = {
|
|
2
|
+
readonly key: K;
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly description?: string;
|
|
5
|
+
readonly default_enabled: boolean;
|
|
6
|
+
readonly parent_key?: K;
|
|
7
|
+
};
|
|
8
|
+
export type PluginFeatureDescriptor<K extends string = string> = {
|
|
9
|
+
readonly plugin_key: string;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly description?: string;
|
|
12
|
+
/** Plugin switch default, `true` when omitted. */
|
|
13
|
+
readonly default_enabled?: boolean;
|
|
14
|
+
readonly features: readonly PluginFeatureDefinition<K>[];
|
|
15
|
+
};
|
|
16
|
+
/** Implemented by a plugin's module service so the base can discover it. */
|
|
17
|
+
export interface PluginFeatureProvider {
|
|
18
|
+
readonly pluginFeatures: PluginFeatureDescriptor;
|
|
19
|
+
}
|
|
20
|
+
export type EffectivePluginFeatures = {
|
|
21
|
+
plugin_key: string;
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
features: Record<string, boolean>;
|
|
24
|
+
};
|
|
25
|
+
/** Configured values as persisted by the base module; `null` = no row. */
|
|
26
|
+
export type StoredPluginFeatures = {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
features: Record<string, boolean>;
|
|
29
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hartl-services/medusa-base",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Base plugin for Hartl Services Medusa shops: per-plugin feature toggles with a generic Admin settings page, a Store endpoint for storefronts, and enforcement helpers for plugin routes, subscribers and jobs.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
".medusa/server",
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://gitlab.com/hartl-services-gmbh/services/food-supplements-e-commerce.git",
|
|
15
|
+
"directory": "plugins/base"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"registry": "https://registry.npmjs.org/"
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": [
|
|
22
|
+
"./.medusa/**"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/public/index.d.ts",
|
|
28
|
+
"import": "./dist/public/index.js",
|
|
29
|
+
"require": "./dist/public/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
|
|
32
|
+
"./.medusa/server/src/*": "./.medusa/server/src/*",
|
|
33
|
+
"./admin": {
|
|
34
|
+
"import": "./.medusa/server/src/admin/index.mjs",
|
|
35
|
+
"require": "./.medusa/server/src/admin/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@medusajs/admin-sdk": "^2.19.0",
|
|
40
|
+
"@medusajs/framework": "^2.19.0",
|
|
41
|
+
"@medusajs/js-sdk": "^2.19.0",
|
|
42
|
+
"@medusajs/medusa": "^2.19.0",
|
|
43
|
+
"@medusajs/ui": "^4.2.1",
|
|
44
|
+
"@tanstack/react-query": "^5.64.2",
|
|
45
|
+
"react": "^18.3.1",
|
|
46
|
+
"react-router-dom": "^7.18.2"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@hartl-services/medusa-food-supplements-contracts": "^0.10.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@medusajs/admin-sdk": "2.19.0",
|
|
53
|
+
"@medusajs/cli": "2.19.0",
|
|
54
|
+
"@medusajs/framework": "2.19.0",
|
|
55
|
+
"@medusajs/js-sdk": "2.19.0",
|
|
56
|
+
"@medusajs/medusa": "2.19.0",
|
|
57
|
+
"@medusajs/ui": "4.2.1",
|
|
58
|
+
"@swc/core": "^1.7.28",
|
|
59
|
+
"@swc/jest": "^0.2.39",
|
|
60
|
+
"@tanstack/react-query": "5.64.2",
|
|
61
|
+
"@types/jest": "^29.5.14",
|
|
62
|
+
"@types/node": "^20.12.11",
|
|
63
|
+
"@types/react": "^18.3.2",
|
|
64
|
+
"@types/react-dom": "^18.2.25",
|
|
65
|
+
"jest": "^29.7.0",
|
|
66
|
+
"react": "^18.3.1",
|
|
67
|
+
"react-router-dom": "7.18.2",
|
|
68
|
+
"typescript": "^5.6.2",
|
|
69
|
+
"vite": "7.3.6",
|
|
70
|
+
"yalc": "^1.0.0-pre.53"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=20"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "medusa plugin:build && tsc -p tsconfig.public.json",
|
|
77
|
+
"dev": "medusa plugin:develop",
|
|
78
|
+
"typecheck": "tsc --noEmit",
|
|
79
|
+
"pretest:unit": "pnpm --filter @hartl-services/medusa-food-supplements-contracts build",
|
|
80
|
+
"test:unit": "jest"
|
|
81
|
+
}
|
|
82
|
+
}
|