@lorion-org/provider-selection 1.0.0-beta.0 → 1.0.0-beta.2

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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the 'Software'), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the 'Software'), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -3,9 +3,12 @@
3
3
  `@lorion-org/provider-selection` is a small framework-free core for selecting one
4
4
  provider per capability from multiple candidates.
5
5
 
6
- It solves four things:
6
+ It solves these things:
7
7
 
8
8
  - collect provider candidates by capability
9
+ - collect provider-owned defaults from descriptors
10
+ - collect provider preferences from descriptors or config-like records
11
+ - collect explicit provider preferences from selected provider ids
9
12
  - optionally collect and resolve in one call
10
13
  - pick one provider with configured and fallback preferences
11
14
  - report misconfigured provider selections
@@ -14,17 +17,20 @@ It solves four things:
14
17
  Selection order is always:
15
18
 
16
19
  1. configured provider
17
- 2. fallback provider
18
- 3. first provider in deterministic sort order
20
+ 2. explicitly selected provider
21
+ 3. fallback provider
22
+ 4. first provider in deterministic sort order
19
23
 
20
24
  If a configured provider is set but not present among the candidates, the package
21
25
  does not silently fall back. It reports a mismatch and leaves that capability
22
26
  unselected.
23
27
 
24
- Example files in this repository:
28
+ Playground examples in this repository:
25
29
 
26
- - `examples/command-handlers.ts`
27
- - `examples/storage-drivers.ts`
30
+ - `packages/nuxt/playground/layer-extensions/payment-provider-stripe/extension.json`
31
+ - `packages/nuxt/playground/layer-extensions/payment-provider-invoice/extension.json`
32
+ - `packages/react/playground/capabilities/payment-provider-stripe/capability.json`
33
+ - `packages/react/playground/capabilities/payment-provider-invoice/capability.json`
28
34
 
29
35
  It does not know anything about:
30
36
 
@@ -42,23 +48,77 @@ pnpm add @lorion-org/provider-selection
42
48
 
43
49
  ## Example
44
50
 
51
+ Use provider-owned defaults when a provider descriptor should decide the normal
52
+ provider for a capability:
53
+
54
+ ```ts
55
+ import {
56
+ collectProviderDefaults,
57
+ resolveItemProviderSelection,
58
+ } from '@lorion-org/provider-selection';
59
+
60
+ const descriptors = [
61
+ { id: 'payment-provider-stripe', providesFor: 'checkout', defaultFor: 'checkout' },
62
+ { id: 'payment-provider-invoice', providesFor: 'checkout' },
63
+ ];
64
+
65
+ const fallbackProviders = collectProviderDefaults({
66
+ items: descriptors,
67
+ getDefaultFor: (item) => item.defaultFor,
68
+ getProviderId: (item) => item.id,
69
+ });
70
+
71
+ const result = resolveItemProviderSelection({
72
+ items: descriptors,
73
+ getCapabilityId: (item) => item.providesFor,
74
+ getProviderId: (item) => item.id,
75
+ fallbackProviders,
76
+ });
77
+ ```
78
+
79
+ This selects Stripe with mode `fallback`. `providesFor` and `defaultFor` both
80
+ accept a string or string array, so one provider can serve multiple capabilities.
81
+
82
+ Use explicit selected providers for seed-owned overrides. This is useful when a
83
+ host app lets users select descriptors through a normal feature or profile seed,
84
+ and a selected descriptor is also a provider for a capability:
85
+
86
+ ```ts
87
+ import {
88
+ collectSelectedProviderPreferences,
89
+ resolveItemProviderSelection,
90
+ } from '@lorion-org/provider-selection';
91
+
92
+ const selectedProviders = collectSelectedProviderPreferences({
93
+ items: descriptors,
94
+ getCapabilityId: (descriptor) => descriptor.providesFor,
95
+ getProviderId: (descriptor) => descriptor.id,
96
+ selectedProviderIds: ['payment-provider-invoice'],
97
+ });
98
+
99
+ const result = resolveItemProviderSelection({
100
+ items: descriptors,
101
+ getCapabilityId: (descriptor) => descriptor.providesFor,
102
+ getProviderId: (descriptor) => descriptor.id,
103
+ fallbackProviders,
104
+ selectedProviders,
105
+ });
106
+ ```
107
+
108
+ This selects Invoice with mode `selected`, even though Stripe owns the fallback
109
+ default. Configured providers still have higher priority.
110
+
111
+ Use configured providers for deployment-owned overrides:
112
+
45
113
  ```ts
46
114
  import { resolveItemProviderSelection } from '@lorion-org/provider-selection';
47
115
 
48
116
  const result = resolveItemProviderSelection({
49
- items: [
50
- { capability: 'auth', providerId: 'keycloak' },
51
- { capability: 'auth', providerId: 'auth-local-jwt' },
52
- { capability: 'mailer', providerId: 'mailer-postmark' },
53
- ],
54
- getCapabilityId: (item) => item.capability,
55
- getProviderId: (item) => item.providerId,
56
- configuredProviders: {
57
- auth: 'keycloak',
58
- },
59
- fallbackProviders: {
60
- mailer: 'mailer-postmark',
61
- },
117
+ items: descriptors,
118
+ getCapabilityId: (item) => item.providesFor,
119
+ getProviderId: (item) => item.id,
120
+ configuredProviders: { checkout: 'payment-provider-invoice' },
121
+ fallbackProviders,
62
122
  });
63
123
 
64
124
  result.selections;
@@ -74,51 +134,46 @@ resolver directly:
74
134
  import { resolveProviderSelection } from '@lorion-org/provider-selection';
75
135
 
76
136
  const result = resolveProviderSelection({
77
- providersByCapability: new Map([['auth', ['auth-local-jwt', 'keycloak']]]),
137
+ providersByCapability: new Map([
138
+ ['checkout', ['payment-provider-invoice', 'payment-provider-stripe']],
139
+ ]),
78
140
  configuredProviders: {
79
- auth: 'missing-provider',
141
+ checkout: 'missing-provider',
80
142
  },
81
143
  });
82
144
 
83
145
  result.selections;
84
146
  result.mismatches;
85
- // => [{ capabilityId: 'auth', configuredProviderId: 'missing-provider' }]
147
+ // => [{ capabilityId: 'checkout', configuredProviderId: 'missing-provider' }]
86
148
  ```
87
149
 
88
- ## Example: command handlers
150
+ If profile or descriptor preferences are stored on descriptors, collect them as
151
+ fallback preferences before resolving:
89
152
 
90
153
  ```ts
91
- import { resolveItemProviderSelection } from '@lorion-org/provider-selection';
92
-
93
- const result = resolveItemProviderSelection({
94
- items: [
95
- { commandId: 'open', handlerId: 'open-native' },
96
- { commandId: 'open', handlerId: 'open-web' },
97
- { commandId: 'share', handlerId: 'share-link' },
98
- ],
99
- getCapabilityId: (item) => item.commandId,
100
- getProviderId: (item) => item.handlerId,
101
- configuredProviders: {
102
- open: 'open-web',
103
- },
154
+ import {
155
+ collectProviderDefaults,
156
+ collectProviderPreferences,
157
+ resolveItemProviderSelection,
158
+ } from '@lorion-org/provider-selection';
159
+
160
+ const providerDefaults = collectProviderDefaults({
161
+ items: descriptors,
162
+ getDefaultFor: (descriptor) => descriptor.defaultFor,
163
+ getProviderId: (descriptor) => descriptor.id,
164
+ });
165
+ const descriptorPreferences = collectProviderPreferences({
166
+ items: descriptors,
167
+ getProviderPreferences: (descriptor) => descriptor.providerPreferences,
104
168
  });
105
- ```
106
-
107
- ## Example: storage drivers
108
-
109
- ```ts
110
- import { resolveItemProviderSelection } from '@lorion-org/provider-selection';
111
169
 
112
170
  const result = resolveItemProviderSelection({
113
- items: [
114
- { storageKind: 'blob', driverId: 's3' },
115
- { storageKind: 'blob', driverId: 'filesystem' },
116
- { storageKind: 'queue', driverId: 'redis-streams' },
117
- ],
118
- getCapabilityId: (item) => item.storageKind,
119
- getProviderId: (item) => item.driverId,
171
+ items: descriptors,
172
+ getCapabilityId: (descriptor) => descriptor.providesFor,
173
+ getProviderId: (descriptor) => descriptor.id,
120
174
  fallbackProviders: {
121
- blob: 'filesystem',
175
+ ...providerDefaults,
176
+ ...descriptorPreferences,
122
177
  },
123
178
  });
124
179
  ```
@@ -128,9 +183,22 @@ const result = resolveItemProviderSelection({
128
183
  ```ts
129
184
  type CapabilityId = string;
130
185
  type ProviderId = string;
131
- type ProviderSelectionMode = 'configured' | 'fallback' | 'first';
186
+ type ProviderSelectionMode = 'configured' | 'selected' | 'fallback' | 'first';
132
187
  type ProviderPreferenceMap = Partial<Record<CapabilityId, ProviderId>>;
133
188
  type ProvidersByCapability = Map<CapabilityId, ProviderId[]>;
189
+ type ProviderCollectionInput<T> = {
190
+ items: Iterable<T>;
191
+ getCapabilityId: (item: T) => unknown;
192
+ getProviderId: (item: T) => ProviderId;
193
+ };
194
+ type SelectedProviderPreferenceCollectionInput<T> = ProviderCollectionInput<T> & {
195
+ selectedProviderIds: Iterable<ProviderId>;
196
+ };
197
+ type ProviderDefaultCollectionInput<T> = {
198
+ items: Iterable<T>;
199
+ getDefaultFor: (item: T) => unknown;
200
+ getProviderId: (item: T) => ProviderId;
201
+ };
134
202
 
135
203
  type ProviderSelection = {
136
204
  capabilityId: CapabilityId;
@@ -157,8 +225,12 @@ type ItemProviderSelectionResolution = ProviderSelectionResolution & {
157
225
 
158
226
  The package exposes:
159
227
 
228
+ - `collectProviderDefaults()`
229
+ - `collectProviderPreferences()`
160
230
  - `collectProvidersByCapability()`
231
+ - `collectSelectedProviderPreferences()`
161
232
  - `resolveItemProviderSelection()`
233
+ - `resolveSelectedProviderRelationPreferences()`
162
234
  - `resolveProviderSelection()`
163
235
 
164
236
  ## Local commands
package/dist/index.cjs CHANGED
@@ -20,14 +20,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ collectProviderDefaults: () => collectProviderDefaults,
24
+ collectProviderPreferences: () => collectProviderPreferences,
23
25
  collectProvidersByCapability: () => collectProvidersByCapability,
26
+ collectSelectedProviderPreferences: () => collectSelectedProviderPreferences,
24
27
  resolveItemProviderSelection: () => resolveItemProviderSelection,
25
- resolveProviderSelection: () => resolveProviderSelection
28
+ resolveProviderSelection: () => resolveProviderSelection,
29
+ resolveSelectedProviderRelationPreferences: () => resolveSelectedProviderRelationPreferences
26
30
  });
27
31
  module.exports = __toCommonJS(index_exports);
28
32
  function toSortedUniqueProviderIds(providerIds) {
29
33
  return Array.from(new Set(Array.from(providerIds).filter(Boolean))).sort();
30
34
  }
35
+ function toCapabilityIds(value) {
36
+ const entries = Array.isArray(value) ? value : [value];
37
+ return entries.filter((entry) => typeof entry === "string" && entry.length > 0);
38
+ }
39
+ function isRecord(value) {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
31
42
  function getSelectedProvider(input) {
32
43
  const firstProviderId = input.candidateProviderIds[0];
33
44
  if (!firstProviderId) {
@@ -43,6 +54,13 @@ function getSelectedProvider(input) {
43
54
  }
44
55
  return void 0;
45
56
  }
57
+ const selectedProviderId = input.selectedProviders?.[input.capabilityId];
58
+ if (selectedProviderId && input.candidateProviderIds.includes(selectedProviderId)) {
59
+ return {
60
+ selectedProviderId,
61
+ mode: "selected"
62
+ };
63
+ }
46
64
  const fallbackProviderId = input.fallbackProviders?.[input.capabilityId];
47
65
  if (fallbackProviderId && input.candidateProviderIds.includes(fallbackProviderId)) {
48
66
  return {
@@ -65,7 +83,8 @@ function selectProviders(input) {
65
83
  capabilityId,
66
84
  candidateProviderIds: normalizedCandidateProviderIds,
67
85
  ...input.configuredProviders ? { configuredProviders: input.configuredProviders } : {},
68
- ...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}
86
+ ...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {},
87
+ ...input.selectedProviders ? { selectedProviders: input.selectedProviders } : {}
69
88
  });
70
89
  if (!selected) {
71
90
  continue;
@@ -120,17 +139,78 @@ function getExcludedProviders(selections) {
120
139
  function collectProvidersByCapability(input) {
121
140
  const providersByCapability = /* @__PURE__ */ new Map();
122
141
  for (const item of input.items) {
123
- const capabilityId = input.getCapabilityId(item);
124
- if (!capabilityId) {
125
- continue;
126
- }
127
142
  const providerId = input.getProviderId(item);
128
- const currentProviderIds = providersByCapability.get(capabilityId) ?? [];
129
- currentProviderIds.push(providerId);
130
- providersByCapability.set(capabilityId, toSortedUniqueProviderIds(currentProviderIds));
143
+ const capabilityIds = toCapabilityIds(input.getCapabilityId(item));
144
+ for (const capabilityId of capabilityIds) {
145
+ const currentProviderIds = providersByCapability.get(capabilityId) ?? [];
146
+ currentProviderIds.push(providerId);
147
+ providersByCapability.set(capabilityId, toSortedUniqueProviderIds(currentProviderIds));
148
+ }
131
149
  }
132
150
  return providersByCapability;
133
151
  }
152
+ function collectProviderPreferences(input) {
153
+ let preferences = {};
154
+ for (const item of input.items) {
155
+ const value = input.getProviderPreferences(item);
156
+ if (!isRecord(value)) continue;
157
+ preferences = {
158
+ ...preferences,
159
+ ...Object.fromEntries(
160
+ Object.entries(value).filter(
161
+ (entry) => typeof entry[1] === "string" && entry[1].length > 0
162
+ )
163
+ )
164
+ };
165
+ }
166
+ return preferences;
167
+ }
168
+ function normalizeDefaultFor(value) {
169
+ const entries = Array.isArray(value) ? value : [value];
170
+ return entries.filter((entry) => typeof entry === "string" && entry.length > 0);
171
+ }
172
+ function collectProviderDefaults(input) {
173
+ let defaults = {};
174
+ for (const item of input.items) {
175
+ const providerId = input.getProviderId(item);
176
+ const capabilityIds = normalizeDefaultFor(input.getDefaultFor(item));
177
+ defaults = {
178
+ ...defaults,
179
+ ...Object.fromEntries(capabilityIds.map((capabilityId) => [capabilityId, providerId]))
180
+ };
181
+ }
182
+ return defaults;
183
+ }
184
+ function collectSelectedProviderPreferences(input) {
185
+ const selectedProviderIds = new Set(input.selectedProviderIds);
186
+ const selectedItems = Array.from(input.items).filter((item) => selectedProviderIds.has(input.getProviderId(item))).sort((left, right) => input.getProviderId(left).localeCompare(input.getProviderId(right)));
187
+ const preferences = {};
188
+ for (const item of selectedItems) {
189
+ const providerId = input.getProviderId(item);
190
+ for (const capabilityId of toCapabilityIds(input.getCapabilityId(item))) {
191
+ if (!preferences[capabilityId]) {
192
+ preferences[capabilityId] = providerId;
193
+ }
194
+ }
195
+ }
196
+ return preferences;
197
+ }
198
+ function resolveSelectedProviderRelationPreferences(input) {
199
+ const selectedProviders = input.selectedProviders ?? {};
200
+ const selectedCapabilityIds = new Set(Object.keys(selectedProviders));
201
+ const defaultFor = toCapabilityIds(input.defaultFor).filter(
202
+ (capabilityId) => !selectedProviders[capabilityId] || selectedProviders[capabilityId] === input.providerId
203
+ );
204
+ const providerPreferences = Object.fromEntries(
205
+ Object.entries(input.providerPreferences ?? {}).filter(
206
+ ([capabilityId]) => !selectedCapabilityIds.has(capabilityId)
207
+ )
208
+ );
209
+ return {
210
+ ...defaultFor.length ? { defaultFor: Array.isArray(input.defaultFor) ? defaultFor : defaultFor[0] } : {},
211
+ ...Object.keys(providerPreferences).length ? { providerPreferences } : {}
212
+ };
213
+ }
134
214
  function resolveProviderSelection(input) {
135
215
  const selections = selectProviders(input);
136
216
  const mismatches = findConfiguredProviderMismatches(input);
@@ -146,7 +226,8 @@ function resolveItemProviderSelection(input) {
146
226
  const resolution = resolveProviderSelection({
147
227
  providersByCapability,
148
228
  ...input.configuredProviders ? { configuredProviders: input.configuredProviders } : {},
149
- ...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}
229
+ ...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {},
230
+ ...input.selectedProviders ? { selectedProviders: input.selectedProviders } : {}
150
231
  });
151
232
  return {
152
233
  providersByCapability,
@@ -155,7 +236,11 @@ function resolveItemProviderSelection(input) {
155
236
  }
156
237
  // Annotate the CommonJS export names for ESM import in node:
157
238
  0 && (module.exports = {
239
+ collectProviderDefaults,
240
+ collectProviderPreferences,
158
241
  collectProvidersByCapability,
242
+ collectSelectedProviderPreferences,
159
243
  resolveItemProviderSelection,
160
- resolveProviderSelection
244
+ resolveProviderSelection,
245
+ resolveSelectedProviderRelationPreferences
161
246
  });
package/dist/index.d.cts CHANGED
@@ -1,7 +1,29 @@
1
1
  type CapabilityId = string;
2
2
  type ProviderId = string;
3
- type ProviderSelectionMode = 'configured' | 'fallback' | 'first';
3
+ type ProviderSelectionMode = 'configured' | 'selected' | 'fallback' | 'first';
4
4
  type ProviderPreferenceMap = Partial<Record<CapabilityId, ProviderId>>;
5
+ type ProviderPreferenceCollectionInput<T> = {
6
+ items: Iterable<T>;
7
+ getProviderPreferences: (item: T) => unknown;
8
+ };
9
+ type ProviderDefaultCollectionInput<T> = {
10
+ items: Iterable<T>;
11
+ getDefaultFor: (item: T) => unknown;
12
+ getProviderId: (item: T) => ProviderId;
13
+ };
14
+ type SelectedProviderPreferenceCollectionInput<T> = ProviderCollectionInput<T> & {
15
+ selectedProviderIds: Iterable<ProviderId>;
16
+ };
17
+ type SelectedProviderRelationPreferenceInput = {
18
+ defaultFor?: CapabilityId | CapabilityId[] | undefined;
19
+ providerId: ProviderId;
20
+ providerPreferences?: ProviderPreferenceMap | undefined;
21
+ selectedProviders?: ProviderPreferenceMap;
22
+ };
23
+ type SelectedProviderRelationPreferences = {
24
+ defaultFor?: CapabilityId | CapabilityId[];
25
+ providerPreferences?: ProviderPreferenceMap;
26
+ };
5
27
  type ProviderSelection = {
6
28
  capabilityId: CapabilityId;
7
29
  selectedProviderId: ProviderId;
@@ -23,20 +45,26 @@ type ItemProviderSelectionResolution = ProviderSelectionResolution & {
23
45
  };
24
46
  type ProviderCollectionInput<T> = {
25
47
  items: Iterable<T>;
26
- getCapabilityId: (item: T) => CapabilityId | undefined;
48
+ getCapabilityId: (item: T) => CapabilityId | CapabilityId[] | undefined;
27
49
  getProviderId: (item: T) => ProviderId;
28
50
  };
29
51
  type ResolveProviderSelectionInput = {
30
52
  providersByCapability: ProvidersByCapability;
31
53
  configuredProviders?: ProviderPreferenceMap;
32
54
  fallbackProviders?: ProviderPreferenceMap;
55
+ selectedProviders?: ProviderPreferenceMap;
33
56
  };
34
57
  type ResolveItemProviderSelectionInput<T> = ProviderCollectionInput<T> & {
35
58
  configuredProviders?: ProviderPreferenceMap;
36
59
  fallbackProviders?: ProviderPreferenceMap;
60
+ selectedProviders?: ProviderPreferenceMap;
37
61
  };
38
62
  declare function collectProvidersByCapability<T>(input: ProviderCollectionInput<T>): ProvidersByCapability;
63
+ declare function collectProviderPreferences<T>(input: ProviderPreferenceCollectionInput<T>): ProviderPreferenceMap;
64
+ declare function collectProviderDefaults<T>(input: ProviderDefaultCollectionInput<T>): ProviderPreferenceMap;
65
+ declare function collectSelectedProviderPreferences<T>(input: SelectedProviderPreferenceCollectionInput<T>): ProviderPreferenceMap;
66
+ declare function resolveSelectedProviderRelationPreferences(input: SelectedProviderRelationPreferenceInput): SelectedProviderRelationPreferences;
39
67
  declare function resolveProviderSelection(input: ResolveProviderSelectionInput): ProviderSelectionResolution;
40
68
  declare function resolveItemProviderSelection<T>(input: ResolveItemProviderSelectionInput<T>): ItemProviderSelectionResolution;
41
69
 
42
- export { type CapabilityId, type ItemProviderSelectionResolution, type ProviderId, type ProviderMismatch, type ProviderPreferenceMap, type ProviderSelection, type ProviderSelectionMode, type ProviderSelectionResolution, type ProvidersByCapability, type ResolveItemProviderSelectionInput, type ResolveProviderSelectionInput, collectProvidersByCapability, resolveItemProviderSelection, resolveProviderSelection };
70
+ export { type CapabilityId, type ItemProviderSelectionResolution, type ProviderDefaultCollectionInput, type ProviderId, type ProviderMismatch, type ProviderPreferenceCollectionInput, type ProviderPreferenceMap, type ProviderSelection, type ProviderSelectionMode, type ProviderSelectionResolution, type ProvidersByCapability, type ResolveItemProviderSelectionInput, type ResolveProviderSelectionInput, type SelectedProviderPreferenceCollectionInput, type SelectedProviderRelationPreferenceInput, type SelectedProviderRelationPreferences, collectProviderDefaults, collectProviderPreferences, collectProvidersByCapability, collectSelectedProviderPreferences, resolveItemProviderSelection, resolveProviderSelection, resolveSelectedProviderRelationPreferences };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,29 @@
1
1
  type CapabilityId = string;
2
2
  type ProviderId = string;
3
- type ProviderSelectionMode = 'configured' | 'fallback' | 'first';
3
+ type ProviderSelectionMode = 'configured' | 'selected' | 'fallback' | 'first';
4
4
  type ProviderPreferenceMap = Partial<Record<CapabilityId, ProviderId>>;
5
+ type ProviderPreferenceCollectionInput<T> = {
6
+ items: Iterable<T>;
7
+ getProviderPreferences: (item: T) => unknown;
8
+ };
9
+ type ProviderDefaultCollectionInput<T> = {
10
+ items: Iterable<T>;
11
+ getDefaultFor: (item: T) => unknown;
12
+ getProviderId: (item: T) => ProviderId;
13
+ };
14
+ type SelectedProviderPreferenceCollectionInput<T> = ProviderCollectionInput<T> & {
15
+ selectedProviderIds: Iterable<ProviderId>;
16
+ };
17
+ type SelectedProviderRelationPreferenceInput = {
18
+ defaultFor?: CapabilityId | CapabilityId[] | undefined;
19
+ providerId: ProviderId;
20
+ providerPreferences?: ProviderPreferenceMap | undefined;
21
+ selectedProviders?: ProviderPreferenceMap;
22
+ };
23
+ type SelectedProviderRelationPreferences = {
24
+ defaultFor?: CapabilityId | CapabilityId[];
25
+ providerPreferences?: ProviderPreferenceMap;
26
+ };
5
27
  type ProviderSelection = {
6
28
  capabilityId: CapabilityId;
7
29
  selectedProviderId: ProviderId;
@@ -23,20 +45,26 @@ type ItemProviderSelectionResolution = ProviderSelectionResolution & {
23
45
  };
24
46
  type ProviderCollectionInput<T> = {
25
47
  items: Iterable<T>;
26
- getCapabilityId: (item: T) => CapabilityId | undefined;
48
+ getCapabilityId: (item: T) => CapabilityId | CapabilityId[] | undefined;
27
49
  getProviderId: (item: T) => ProviderId;
28
50
  };
29
51
  type ResolveProviderSelectionInput = {
30
52
  providersByCapability: ProvidersByCapability;
31
53
  configuredProviders?: ProviderPreferenceMap;
32
54
  fallbackProviders?: ProviderPreferenceMap;
55
+ selectedProviders?: ProviderPreferenceMap;
33
56
  };
34
57
  type ResolveItemProviderSelectionInput<T> = ProviderCollectionInput<T> & {
35
58
  configuredProviders?: ProviderPreferenceMap;
36
59
  fallbackProviders?: ProviderPreferenceMap;
60
+ selectedProviders?: ProviderPreferenceMap;
37
61
  };
38
62
  declare function collectProvidersByCapability<T>(input: ProviderCollectionInput<T>): ProvidersByCapability;
63
+ declare function collectProviderPreferences<T>(input: ProviderPreferenceCollectionInput<T>): ProviderPreferenceMap;
64
+ declare function collectProviderDefaults<T>(input: ProviderDefaultCollectionInput<T>): ProviderPreferenceMap;
65
+ declare function collectSelectedProviderPreferences<T>(input: SelectedProviderPreferenceCollectionInput<T>): ProviderPreferenceMap;
66
+ declare function resolveSelectedProviderRelationPreferences(input: SelectedProviderRelationPreferenceInput): SelectedProviderRelationPreferences;
39
67
  declare function resolveProviderSelection(input: ResolveProviderSelectionInput): ProviderSelectionResolution;
40
68
  declare function resolveItemProviderSelection<T>(input: ResolveItemProviderSelectionInput<T>): ItemProviderSelectionResolution;
41
69
 
42
- export { type CapabilityId, type ItemProviderSelectionResolution, type ProviderId, type ProviderMismatch, type ProviderPreferenceMap, type ProviderSelection, type ProviderSelectionMode, type ProviderSelectionResolution, type ProvidersByCapability, type ResolveItemProviderSelectionInput, type ResolveProviderSelectionInput, collectProvidersByCapability, resolveItemProviderSelection, resolveProviderSelection };
70
+ export { type CapabilityId, type ItemProviderSelectionResolution, type ProviderDefaultCollectionInput, type ProviderId, type ProviderMismatch, type ProviderPreferenceCollectionInput, type ProviderPreferenceMap, type ProviderSelection, type ProviderSelectionMode, type ProviderSelectionResolution, type ProvidersByCapability, type ResolveItemProviderSelectionInput, type ResolveProviderSelectionInput, type SelectedProviderPreferenceCollectionInput, type SelectedProviderRelationPreferenceInput, type SelectedProviderRelationPreferences, collectProviderDefaults, collectProviderPreferences, collectProvidersByCapability, collectSelectedProviderPreferences, resolveItemProviderSelection, resolveProviderSelection, resolveSelectedProviderRelationPreferences };
package/dist/index.js CHANGED
@@ -2,6 +2,13 @@
2
2
  function toSortedUniqueProviderIds(providerIds) {
3
3
  return Array.from(new Set(Array.from(providerIds).filter(Boolean))).sort();
4
4
  }
5
+ function toCapabilityIds(value) {
6
+ const entries = Array.isArray(value) ? value : [value];
7
+ return entries.filter((entry) => typeof entry === "string" && entry.length > 0);
8
+ }
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
5
12
  function getSelectedProvider(input) {
6
13
  const firstProviderId = input.candidateProviderIds[0];
7
14
  if (!firstProviderId) {
@@ -17,6 +24,13 @@ function getSelectedProvider(input) {
17
24
  }
18
25
  return void 0;
19
26
  }
27
+ const selectedProviderId = input.selectedProviders?.[input.capabilityId];
28
+ if (selectedProviderId && input.candidateProviderIds.includes(selectedProviderId)) {
29
+ return {
30
+ selectedProviderId,
31
+ mode: "selected"
32
+ };
33
+ }
20
34
  const fallbackProviderId = input.fallbackProviders?.[input.capabilityId];
21
35
  if (fallbackProviderId && input.candidateProviderIds.includes(fallbackProviderId)) {
22
36
  return {
@@ -39,7 +53,8 @@ function selectProviders(input) {
39
53
  capabilityId,
40
54
  candidateProviderIds: normalizedCandidateProviderIds,
41
55
  ...input.configuredProviders ? { configuredProviders: input.configuredProviders } : {},
42
- ...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}
56
+ ...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {},
57
+ ...input.selectedProviders ? { selectedProviders: input.selectedProviders } : {}
43
58
  });
44
59
  if (!selected) {
45
60
  continue;
@@ -94,17 +109,78 @@ function getExcludedProviders(selections) {
94
109
  function collectProvidersByCapability(input) {
95
110
  const providersByCapability = /* @__PURE__ */ new Map();
96
111
  for (const item of input.items) {
97
- const capabilityId = input.getCapabilityId(item);
98
- if (!capabilityId) {
99
- continue;
100
- }
101
112
  const providerId = input.getProviderId(item);
102
- const currentProviderIds = providersByCapability.get(capabilityId) ?? [];
103
- currentProviderIds.push(providerId);
104
- providersByCapability.set(capabilityId, toSortedUniqueProviderIds(currentProviderIds));
113
+ const capabilityIds = toCapabilityIds(input.getCapabilityId(item));
114
+ for (const capabilityId of capabilityIds) {
115
+ const currentProviderIds = providersByCapability.get(capabilityId) ?? [];
116
+ currentProviderIds.push(providerId);
117
+ providersByCapability.set(capabilityId, toSortedUniqueProviderIds(currentProviderIds));
118
+ }
105
119
  }
106
120
  return providersByCapability;
107
121
  }
122
+ function collectProviderPreferences(input) {
123
+ let preferences = {};
124
+ for (const item of input.items) {
125
+ const value = input.getProviderPreferences(item);
126
+ if (!isRecord(value)) continue;
127
+ preferences = {
128
+ ...preferences,
129
+ ...Object.fromEntries(
130
+ Object.entries(value).filter(
131
+ (entry) => typeof entry[1] === "string" && entry[1].length > 0
132
+ )
133
+ )
134
+ };
135
+ }
136
+ return preferences;
137
+ }
138
+ function normalizeDefaultFor(value) {
139
+ const entries = Array.isArray(value) ? value : [value];
140
+ return entries.filter((entry) => typeof entry === "string" && entry.length > 0);
141
+ }
142
+ function collectProviderDefaults(input) {
143
+ let defaults = {};
144
+ for (const item of input.items) {
145
+ const providerId = input.getProviderId(item);
146
+ const capabilityIds = normalizeDefaultFor(input.getDefaultFor(item));
147
+ defaults = {
148
+ ...defaults,
149
+ ...Object.fromEntries(capabilityIds.map((capabilityId) => [capabilityId, providerId]))
150
+ };
151
+ }
152
+ return defaults;
153
+ }
154
+ function collectSelectedProviderPreferences(input) {
155
+ const selectedProviderIds = new Set(input.selectedProviderIds);
156
+ const selectedItems = Array.from(input.items).filter((item) => selectedProviderIds.has(input.getProviderId(item))).sort((left, right) => input.getProviderId(left).localeCompare(input.getProviderId(right)));
157
+ const preferences = {};
158
+ for (const item of selectedItems) {
159
+ const providerId = input.getProviderId(item);
160
+ for (const capabilityId of toCapabilityIds(input.getCapabilityId(item))) {
161
+ if (!preferences[capabilityId]) {
162
+ preferences[capabilityId] = providerId;
163
+ }
164
+ }
165
+ }
166
+ return preferences;
167
+ }
168
+ function resolveSelectedProviderRelationPreferences(input) {
169
+ const selectedProviders = input.selectedProviders ?? {};
170
+ const selectedCapabilityIds = new Set(Object.keys(selectedProviders));
171
+ const defaultFor = toCapabilityIds(input.defaultFor).filter(
172
+ (capabilityId) => !selectedProviders[capabilityId] || selectedProviders[capabilityId] === input.providerId
173
+ );
174
+ const providerPreferences = Object.fromEntries(
175
+ Object.entries(input.providerPreferences ?? {}).filter(
176
+ ([capabilityId]) => !selectedCapabilityIds.has(capabilityId)
177
+ )
178
+ );
179
+ return {
180
+ ...defaultFor.length ? { defaultFor: Array.isArray(input.defaultFor) ? defaultFor : defaultFor[0] } : {},
181
+ ...Object.keys(providerPreferences).length ? { providerPreferences } : {}
182
+ };
183
+ }
108
184
  function resolveProviderSelection(input) {
109
185
  const selections = selectProviders(input);
110
186
  const mismatches = findConfiguredProviderMismatches(input);
@@ -120,7 +196,8 @@ function resolveItemProviderSelection(input) {
120
196
  const resolution = resolveProviderSelection({
121
197
  providersByCapability,
122
198
  ...input.configuredProviders ? { configuredProviders: input.configuredProviders } : {},
123
- ...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}
199
+ ...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {},
200
+ ...input.selectedProviders ? { selectedProviders: input.selectedProviders } : {}
124
201
  });
125
202
  return {
126
203
  providersByCapability,
@@ -128,7 +205,11 @@ function resolveItemProviderSelection(input) {
128
205
  };
129
206
  }
130
207
  export {
208
+ collectProviderDefaults,
209
+ collectProviderPreferences,
131
210
  collectProvidersByCapability,
211
+ collectSelectedProviderPreferences,
132
212
  resolveItemProviderSelection,
133
- resolveProviderSelection
213
+ resolveProviderSelection,
214
+ resolveSelectedProviderRelationPreferences
134
215
  };
@@ -0,0 +1,38 @@
1
+ import { resolveItemProviderSelection } from '@lorion-org/provider-selection';
2
+
3
+ type PaymentProviderDescriptor = {
4
+ id: string;
5
+ providesFor?: string | string[];
6
+ };
7
+
8
+ const descriptors: PaymentProviderDescriptor[] = [
9
+ {
10
+ id: 'payment-provider-stripe',
11
+ providesFor: 'checkout',
12
+ },
13
+ {
14
+ id: 'payment-provider-invoice',
15
+ providesFor: 'checkout',
16
+ },
17
+ ];
18
+
19
+ const result = resolveItemProviderSelection({
20
+ items: descriptors,
21
+ getCapabilityId: (descriptor) => descriptor.providesFor,
22
+ getProviderId: (descriptor) => descriptor.id,
23
+ configuredProviders: {
24
+ checkout: 'payment-provider-stripe',
25
+ },
26
+ });
27
+
28
+ console.log(result.providersByCapability);
29
+ // Map { checkout => ['payment-provider-invoice', 'payment-provider-stripe'] }
30
+
31
+ console.log(result.selections);
32
+ // Map { checkout => { selectedProviderId: 'payment-provider-stripe', mode: 'configured', ... } }
33
+
34
+ console.log(result.mismatches);
35
+ // []
36
+
37
+ console.log(result.excludedProviderIds);
38
+ // ['payment-provider-invoice']
@@ -0,0 +1,61 @@
1
+ import {
2
+ collectProviderDefaults,
3
+ collectProviderPreferences,
4
+ collectSelectedProviderPreferences,
5
+ resolveItemProviderSelection,
6
+ } from '@lorion-org/provider-selection';
7
+
8
+ type PlaygroundDescriptor = {
9
+ defaultFor?: string | string[];
10
+ id: string;
11
+ providerPreferences?: Record<string, string>;
12
+ providesFor?: string | string[];
13
+ };
14
+
15
+ const descriptors: PlaygroundDescriptor[] = [
16
+ {
17
+ id: 'web',
18
+ providerPreferences: {
19
+ checkout: 'payment-provider-stripe',
20
+ },
21
+ },
22
+ {
23
+ id: 'payment-provider-stripe',
24
+ defaultFor: 'checkout',
25
+ providesFor: 'checkout',
26
+ },
27
+ {
28
+ id: 'payment-provider-invoice',
29
+ providesFor: 'checkout',
30
+ },
31
+ ];
32
+
33
+ const providerDefaults = collectProviderDefaults({
34
+ items: descriptors,
35
+ getDefaultFor: (descriptor) => descriptor.defaultFor,
36
+ getProviderId: (descriptor) => descriptor.id,
37
+ });
38
+ const descriptorPreferences = collectProviderPreferences({
39
+ items: descriptors,
40
+ getProviderPreferences: (descriptor) => descriptor.providerPreferences,
41
+ });
42
+ const selectedProviders = collectSelectedProviderPreferences({
43
+ items: descriptors,
44
+ getCapabilityId: (descriptor) => descriptor.providesFor,
45
+ getProviderId: (descriptor) => descriptor.id,
46
+ selectedProviderIds: ['payment-provider-invoice'],
47
+ });
48
+
49
+ const result = resolveItemProviderSelection({
50
+ items: descriptors,
51
+ getCapabilityId: (descriptor) => descriptor.providesFor,
52
+ getProviderId: (descriptor) => descriptor.id,
53
+ fallbackProviders: {
54
+ ...providerDefaults,
55
+ ...descriptorPreferences,
56
+ },
57
+ selectedProviders,
58
+ });
59
+
60
+ console.log(Object.fromEntries(result.selections));
61
+ // { checkout: { selectedProviderId: 'payment-provider-invoice', mode: 'selected', ... } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lorion-org/provider-selection",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Framework-free capability provider selection primitives.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,6 +22,10 @@
22
22
  "types": "./dist/index.d.ts",
23
23
  "exports": {
24
24
  ".": {
25
+ "lorion-source": {
26
+ "types": "./src/index.ts",
27
+ "default": "./src/index.ts"
28
+ },
25
29
  "import": {
26
30
  "types": "./dist/index.d.ts",
27
31
  "default": "./dist/index.js"
@@ -35,7 +39,9 @@
35
39
  "files": [
36
40
  "dist",
37
41
  "examples",
38
- "LICENSE"
42
+ "LICENSE",
43
+ "src/**/*.ts",
44
+ "!src/**/*.spec.ts"
39
45
  ],
40
46
  "keywords": [
41
47
  "provider",
package/src/index.ts ADDED
@@ -0,0 +1,355 @@
1
+ export type CapabilityId = string;
2
+ export type ProviderId = string;
3
+
4
+ export type ProviderSelectionMode = 'configured' | 'selected' | 'fallback' | 'first';
5
+ export type ProviderPreferenceMap = Partial<Record<CapabilityId, ProviderId>>;
6
+ export type ProviderPreferenceCollectionInput<T> = {
7
+ items: Iterable<T>;
8
+ getProviderPreferences: (item: T) => unknown;
9
+ };
10
+ export type ProviderDefaultCollectionInput<T> = {
11
+ items: Iterable<T>;
12
+ getDefaultFor: (item: T) => unknown;
13
+ getProviderId: (item: T) => ProviderId;
14
+ };
15
+ export type SelectedProviderPreferenceCollectionInput<T> = ProviderCollectionInput<T> & {
16
+ selectedProviderIds: Iterable<ProviderId>;
17
+ };
18
+ export type SelectedProviderRelationPreferenceInput = {
19
+ defaultFor?: CapabilityId | CapabilityId[] | undefined;
20
+ providerId: ProviderId;
21
+ providerPreferences?: ProviderPreferenceMap | undefined;
22
+ selectedProviders?: ProviderPreferenceMap;
23
+ };
24
+
25
+ export type SelectedProviderRelationPreferences = {
26
+ defaultFor?: CapabilityId | CapabilityId[];
27
+ providerPreferences?: ProviderPreferenceMap;
28
+ };
29
+
30
+ export type ProviderSelection = {
31
+ capabilityId: CapabilityId;
32
+ selectedProviderId: ProviderId;
33
+ candidateProviderIds: ProviderId[];
34
+ mode: ProviderSelectionMode;
35
+ };
36
+
37
+ export type ProviderMismatch = {
38
+ capabilityId: CapabilityId;
39
+ configuredProviderId: ProviderId;
40
+ };
41
+
42
+ export type ProvidersByCapability = Map<CapabilityId, ProviderId[]>;
43
+
44
+ export type ProviderSelectionResolution = {
45
+ selections: Map<CapabilityId, ProviderSelection>;
46
+ mismatches: ProviderMismatch[];
47
+ excludedProviderIds: ProviderId[];
48
+ };
49
+ export type ItemProviderSelectionResolution = ProviderSelectionResolution & {
50
+ providersByCapability: ProvidersByCapability;
51
+ };
52
+
53
+ type ProviderCollectionInput<T> = {
54
+ items: Iterable<T>;
55
+ getCapabilityId: (item: T) => CapabilityId | CapabilityId[] | undefined;
56
+ getProviderId: (item: T) => ProviderId;
57
+ };
58
+
59
+ export type ResolveProviderSelectionInput = {
60
+ providersByCapability: ProvidersByCapability;
61
+ configuredProviders?: ProviderPreferenceMap;
62
+ fallbackProviders?: ProviderPreferenceMap;
63
+ selectedProviders?: ProviderPreferenceMap;
64
+ };
65
+
66
+ export type ResolveItemProviderSelectionInput<T> = ProviderCollectionInput<T> & {
67
+ configuredProviders?: ProviderPreferenceMap;
68
+ fallbackProviders?: ProviderPreferenceMap;
69
+ selectedProviders?: ProviderPreferenceMap;
70
+ };
71
+
72
+ function toSortedUniqueProviderIds(providerIds: Iterable<ProviderId>): ProviderId[] {
73
+ return Array.from(new Set(Array.from(providerIds).filter(Boolean))).sort();
74
+ }
75
+
76
+ function toCapabilityIds(value: CapabilityId | CapabilityId[] | undefined): CapabilityId[] {
77
+ const entries = Array.isArray(value) ? value : [value];
78
+
79
+ return entries.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
80
+ }
81
+
82
+ function isRecord(value: unknown): value is Record<string, unknown> {
83
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
84
+ }
85
+
86
+ function getSelectedProvider(input: {
87
+ capabilityId: CapabilityId;
88
+ candidateProviderIds: ProviderId[];
89
+ configuredProviders?: ProviderPreferenceMap;
90
+ fallbackProviders?: ProviderPreferenceMap;
91
+ selectedProviders?: ProviderPreferenceMap;
92
+ }): { selectedProviderId: ProviderId; mode: ProviderSelectionMode } | undefined {
93
+ const firstProviderId = input.candidateProviderIds[0];
94
+ if (!firstProviderId) {
95
+ return undefined;
96
+ }
97
+
98
+ const configuredProviderId = input.configuredProviders?.[input.capabilityId];
99
+ if (configuredProviderId) {
100
+ if (input.candidateProviderIds.includes(configuredProviderId)) {
101
+ return {
102
+ selectedProviderId: configuredProviderId,
103
+ mode: 'configured',
104
+ };
105
+ }
106
+
107
+ return undefined;
108
+ }
109
+
110
+ const selectedProviderId = input.selectedProviders?.[input.capabilityId];
111
+ if (selectedProviderId && input.candidateProviderIds.includes(selectedProviderId)) {
112
+ return {
113
+ selectedProviderId,
114
+ mode: 'selected',
115
+ };
116
+ }
117
+
118
+ const fallbackProviderId = input.fallbackProviders?.[input.capabilityId];
119
+ if (fallbackProviderId && input.candidateProviderIds.includes(fallbackProviderId)) {
120
+ return {
121
+ selectedProviderId: fallbackProviderId,
122
+ mode: 'fallback',
123
+ };
124
+ }
125
+
126
+ return {
127
+ selectedProviderId: firstProviderId,
128
+ mode: 'first',
129
+ };
130
+ }
131
+
132
+ function selectProviders(
133
+ input: ResolveProviderSelectionInput,
134
+ ): Map<CapabilityId, ProviderSelection> {
135
+ const selections: Map<CapabilityId, ProviderSelection> = new Map();
136
+
137
+ for (const [capabilityId, candidateProviderIds] of Array.from(
138
+ input.providersByCapability.entries(),
139
+ ).sort(([left], [right]) => left.localeCompare(right))) {
140
+ const normalizedCandidateProviderIds = toSortedUniqueProviderIds(candidateProviderIds);
141
+ const selected = getSelectedProvider({
142
+ capabilityId,
143
+ candidateProviderIds: normalizedCandidateProviderIds,
144
+ ...(input.configuredProviders ? { configuredProviders: input.configuredProviders } : {}),
145
+ ...(input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}),
146
+ ...(input.selectedProviders ? { selectedProviders: input.selectedProviders } : {}),
147
+ });
148
+
149
+ if (!selected) {
150
+ continue;
151
+ }
152
+
153
+ selections.set(capabilityId, {
154
+ capabilityId,
155
+ selectedProviderId: selected.selectedProviderId,
156
+ candidateProviderIds: normalizedCandidateProviderIds,
157
+ mode: selected.mode,
158
+ });
159
+ }
160
+
161
+ return selections;
162
+ }
163
+
164
+ function findConfiguredProviderMismatches(
165
+ input: ResolveProviderSelectionInput,
166
+ ): ProviderMismatch[] {
167
+ const mismatches: ProviderMismatch[] = [];
168
+
169
+ for (const [capabilityId, configuredProviderId] of Object.entries(
170
+ input.configuredProviders ?? {},
171
+ ).sort(([left], [right]) => left.localeCompare(right))) {
172
+ if (!configuredProviderId) {
173
+ continue;
174
+ }
175
+
176
+ const candidateProviderIds = toSortedUniqueProviderIds(
177
+ input.providersByCapability.get(capabilityId) ?? [],
178
+ );
179
+ if (!candidateProviderIds.length) {
180
+ continue;
181
+ }
182
+
183
+ if (candidateProviderIds.includes(configuredProviderId)) {
184
+ continue;
185
+ }
186
+
187
+ mismatches.push({
188
+ capabilityId,
189
+ configuredProviderId,
190
+ });
191
+ }
192
+
193
+ return mismatches;
194
+ }
195
+
196
+ function getExcludedProviders(selections: Iterable<ProviderSelection>): ProviderId[] {
197
+ const excludedProviderIds: ProviderId[] = [];
198
+
199
+ for (const selection of selections) {
200
+ if (selection.candidateProviderIds.length <= 1) {
201
+ continue;
202
+ }
203
+
204
+ for (const candidateProviderId of selection.candidateProviderIds) {
205
+ if (candidateProviderId !== selection.selectedProviderId) {
206
+ excludedProviderIds.push(candidateProviderId);
207
+ }
208
+ }
209
+ }
210
+
211
+ return toSortedUniqueProviderIds(excludedProviderIds);
212
+ }
213
+
214
+ export function collectProvidersByCapability<T>(
215
+ input: ProviderCollectionInput<T>,
216
+ ): ProvidersByCapability {
217
+ const providersByCapability: ProvidersByCapability = new Map();
218
+
219
+ for (const item of input.items) {
220
+ const providerId = input.getProviderId(item);
221
+ const capabilityIds = toCapabilityIds(input.getCapabilityId(item));
222
+
223
+ for (const capabilityId of capabilityIds) {
224
+ const currentProviderIds = providersByCapability.get(capabilityId) ?? [];
225
+ currentProviderIds.push(providerId);
226
+ providersByCapability.set(capabilityId, toSortedUniqueProviderIds(currentProviderIds));
227
+ }
228
+ }
229
+
230
+ return providersByCapability;
231
+ }
232
+
233
+ export function collectProviderPreferences<T>(
234
+ input: ProviderPreferenceCollectionInput<T>,
235
+ ): ProviderPreferenceMap {
236
+ let preferences: ProviderPreferenceMap = {};
237
+
238
+ for (const item of input.items) {
239
+ const value = input.getProviderPreferences(item);
240
+
241
+ if (!isRecord(value)) continue;
242
+
243
+ preferences = {
244
+ ...preferences,
245
+ ...Object.fromEntries(
246
+ Object.entries(value).filter(
247
+ (entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].length > 0,
248
+ ),
249
+ ),
250
+ };
251
+ }
252
+
253
+ return preferences;
254
+ }
255
+
256
+ function normalizeDefaultFor(value: unknown): CapabilityId[] {
257
+ const entries = Array.isArray(value) ? value : [value];
258
+
259
+ return entries.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0);
260
+ }
261
+
262
+ export function collectProviderDefaults<T>(
263
+ input: ProviderDefaultCollectionInput<T>,
264
+ ): ProviderPreferenceMap {
265
+ let defaults: ProviderPreferenceMap = {};
266
+
267
+ for (const item of input.items) {
268
+ const providerId = input.getProviderId(item);
269
+ const capabilityIds = normalizeDefaultFor(input.getDefaultFor(item));
270
+
271
+ defaults = {
272
+ ...defaults,
273
+ ...Object.fromEntries(capabilityIds.map((capabilityId) => [capabilityId, providerId])),
274
+ };
275
+ }
276
+
277
+ return defaults;
278
+ }
279
+
280
+ export function collectSelectedProviderPreferences<T>(
281
+ input: SelectedProviderPreferenceCollectionInput<T>,
282
+ ): ProviderPreferenceMap {
283
+ const selectedProviderIds = new Set(input.selectedProviderIds);
284
+ const selectedItems = Array.from(input.items)
285
+ .filter((item) => selectedProviderIds.has(input.getProviderId(item)))
286
+ .sort((left, right) => input.getProviderId(left).localeCompare(input.getProviderId(right)));
287
+ const preferences: ProviderPreferenceMap = {};
288
+
289
+ for (const item of selectedItems) {
290
+ const providerId = input.getProviderId(item);
291
+
292
+ for (const capabilityId of toCapabilityIds(input.getCapabilityId(item))) {
293
+ if (!preferences[capabilityId]) {
294
+ preferences[capabilityId] = providerId;
295
+ }
296
+ }
297
+ }
298
+
299
+ return preferences;
300
+ }
301
+
302
+ export function resolveSelectedProviderRelationPreferences(
303
+ input: SelectedProviderRelationPreferenceInput,
304
+ ): SelectedProviderRelationPreferences {
305
+ const selectedProviders = input.selectedProviders ?? {};
306
+ const selectedCapabilityIds = new Set(Object.keys(selectedProviders));
307
+ const defaultFor = toCapabilityIds(input.defaultFor).filter(
308
+ (capabilityId) =>
309
+ !selectedProviders[capabilityId] || selectedProviders[capabilityId] === input.providerId,
310
+ );
311
+ const providerPreferences = Object.fromEntries(
312
+ Object.entries(input.providerPreferences ?? {}).filter(
313
+ ([capabilityId]) => !selectedCapabilityIds.has(capabilityId),
314
+ ),
315
+ );
316
+
317
+ return {
318
+ ...(defaultFor.length
319
+ ? { defaultFor: Array.isArray(input.defaultFor) ? defaultFor : defaultFor[0] }
320
+ : {}),
321
+ ...(Object.keys(providerPreferences).length ? { providerPreferences } : {}),
322
+ };
323
+ }
324
+
325
+ export function resolveProviderSelection(
326
+ input: ResolveProviderSelectionInput,
327
+ ): ProviderSelectionResolution {
328
+ const selections = selectProviders(input);
329
+ const mismatches = findConfiguredProviderMismatches(input);
330
+ const excludedProviderIds = getExcludedProviders(selections.values());
331
+
332
+ return {
333
+ selections,
334
+ mismatches,
335
+ excludedProviderIds,
336
+ };
337
+ }
338
+
339
+ export function resolveItemProviderSelection<T>(
340
+ input: ResolveItemProviderSelectionInput<T>,
341
+ ): ItemProviderSelectionResolution {
342
+ const providersByCapability = collectProvidersByCapability(input);
343
+
344
+ const resolution = resolveProviderSelection({
345
+ providersByCapability,
346
+ ...(input.configuredProviders ? { configuredProviders: input.configuredProviders } : {}),
347
+ ...(input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}),
348
+ ...(input.selectedProviders ? { selectedProviders: input.selectedProviders } : {}),
349
+ });
350
+
351
+ return {
352
+ providersByCapability,
353
+ ...resolution,
354
+ };
355
+ }
@@ -1,45 +0,0 @@
1
- import { resolveItemProviderSelection } from '@lorion-org/provider-selection';
2
-
3
- type CommandHandler = {
4
- commandId: string;
5
- handlerId: string;
6
- };
7
-
8
- const handlers: CommandHandler[] = [
9
- {
10
- commandId: 'open',
11
- handlerId: 'open-native',
12
- },
13
- {
14
- commandId: 'open',
15
- handlerId: 'open-web',
16
- },
17
- {
18
- commandId: 'share',
19
- handlerId: 'share-link',
20
- },
21
- ];
22
-
23
- const result = resolveItemProviderSelection({
24
- items: handlers,
25
- getCapabilityId: (handler) => handler.commandId,
26
- getProviderId: (handler) => handler.handlerId,
27
- configuredProviders: {
28
- open: 'open-web',
29
- },
30
- });
31
-
32
- console.log(result.providersByCapability);
33
- // { open: ['open-native', 'open-web'], share: ['share-link'] }
34
-
35
- console.log(result.selections);
36
- // {
37
- // open: { selectedProviderId: 'open-web', mode: 'configured' },
38
- // share: { selectedProviderId: 'share-link', mode: 'first' }
39
- // }
40
-
41
- console.log(result.mismatches);
42
- // []
43
-
44
- console.log(result.excludedProviderIds);
45
- // ['open-native']
@@ -1,39 +0,0 @@
1
- import { resolveItemProviderSelection } from '@lorion-org/provider-selection';
2
-
3
- type StorageDriver = {
4
- storageKind: string;
5
- driverId: string;
6
- };
7
-
8
- const drivers: StorageDriver[] = [
9
- {
10
- storageKind: 'blob',
11
- driverId: 's3',
12
- },
13
- {
14
- storageKind: 'blob',
15
- driverId: 'filesystem',
16
- },
17
- {
18
- storageKind: 'queue',
19
- driverId: 'redis-streams',
20
- },
21
- ];
22
-
23
- const result = resolveItemProviderSelection({
24
- items: drivers,
25
- getCapabilityId: (driver) => driver.storageKind,
26
- getProviderId: (driver) => driver.driverId,
27
- fallbackProviders: {
28
- blob: 'filesystem',
29
- },
30
- });
31
-
32
- console.log(result.selections);
33
- // {
34
- // blob: { selectedProviderId: 'filesystem', mode: 'fallback' },
35
- // queue: { selectedProviderId: 'redis-streams', mode: 'first' }
36
- // }
37
-
38
- console.log(result.mismatches);
39
- // []