@lorion-org/provider-selection 1.0.0-beta.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 +173 -0
- package/dist/index.cjs +161 -0
- package/dist/index.d.cts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +134 -0
- package/examples/command-handlers.ts +45 -0
- package/examples/storage-drivers.ts +39 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +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.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @lorion-org/provider-selection
|
|
2
|
+
|
|
3
|
+
`@lorion-org/provider-selection` is a small framework-free core for selecting one
|
|
4
|
+
provider per capability from multiple candidates.
|
|
5
|
+
|
|
6
|
+
It solves four things:
|
|
7
|
+
|
|
8
|
+
- collect provider candidates by capability
|
|
9
|
+
- optionally collect and resolve in one call
|
|
10
|
+
- pick one provider with configured and fallback preferences
|
|
11
|
+
- report misconfigured provider selections
|
|
12
|
+
- return excluded providers that lost the selection
|
|
13
|
+
|
|
14
|
+
Selection order is always:
|
|
15
|
+
|
|
16
|
+
1. configured provider
|
|
17
|
+
2. fallback provider
|
|
18
|
+
3. first provider in deterministic sort order
|
|
19
|
+
|
|
20
|
+
If a configured provider is set but not present among the candidates, the package
|
|
21
|
+
does not silently fall back. It reports a mismatch and leaves that capability
|
|
22
|
+
unselected.
|
|
23
|
+
|
|
24
|
+
Example files in this repository:
|
|
25
|
+
|
|
26
|
+
- `examples/command-handlers.ts`
|
|
27
|
+
- `examples/storage-drivers.ts`
|
|
28
|
+
|
|
29
|
+
It does not know anything about:
|
|
30
|
+
|
|
31
|
+
- framework runtime config
|
|
32
|
+
- feature manifests
|
|
33
|
+
- plugins
|
|
34
|
+
- filesystems
|
|
35
|
+
- application-specific contract names
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```shell
|
|
40
|
+
pnpm add @lorion-org/provider-selection
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { resolveItemProviderSelection } from '@lorion-org/provider-selection';
|
|
47
|
+
|
|
48
|
+
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
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
result.selections;
|
|
65
|
+
result.providersByCapability;
|
|
66
|
+
result.mismatches;
|
|
67
|
+
result.excludedProviderIds;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If you already have a `Map<capability, providers>`, use the lower-level
|
|
71
|
+
resolver directly:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { resolveProviderSelection } from '@lorion-org/provider-selection';
|
|
75
|
+
|
|
76
|
+
const result = resolveProviderSelection({
|
|
77
|
+
providersByCapability: new Map([['auth', ['auth-local-jwt', 'keycloak']]]),
|
|
78
|
+
configuredProviders: {
|
|
79
|
+
auth: 'missing-provider',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
result.selections;
|
|
84
|
+
result.mismatches;
|
|
85
|
+
// => [{ capabilityId: 'auth', configuredProviderId: 'missing-provider' }]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Example: command handlers
|
|
89
|
+
|
|
90
|
+
```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
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Example: storage drivers
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { resolveItemProviderSelection } from '@lorion-org/provider-selection';
|
|
111
|
+
|
|
112
|
+
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,
|
|
120
|
+
fallbackProviders: {
|
|
121
|
+
blob: 'filesystem',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## API
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
type CapabilityId = string;
|
|
130
|
+
type ProviderId = string;
|
|
131
|
+
type ProviderSelectionMode = 'configured' | 'fallback' | 'first';
|
|
132
|
+
type ProviderPreferenceMap = Partial<Record<CapabilityId, ProviderId>>;
|
|
133
|
+
type ProvidersByCapability = Map<CapabilityId, ProviderId[]>;
|
|
134
|
+
|
|
135
|
+
type ProviderSelection = {
|
|
136
|
+
capabilityId: CapabilityId;
|
|
137
|
+
selectedProviderId: ProviderId;
|
|
138
|
+
candidateProviderIds: ProviderId[];
|
|
139
|
+
mode: ProviderSelectionMode;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type ProviderMismatch = {
|
|
143
|
+
capabilityId: CapabilityId;
|
|
144
|
+
configuredProviderId: ProviderId;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type ProviderSelectionResolution = {
|
|
148
|
+
selections: Map<CapabilityId, ProviderSelection>;
|
|
149
|
+
mismatches: ProviderMismatch[];
|
|
150
|
+
excludedProviderIds: ProviderId[];
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
type ItemProviderSelectionResolution = ProviderSelectionResolution & {
|
|
154
|
+
providersByCapability: ProvidersByCapability;
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The package exposes:
|
|
159
|
+
|
|
160
|
+
- `collectProvidersByCapability()`
|
|
161
|
+
- `resolveItemProviderSelection()`
|
|
162
|
+
- `resolveProviderSelection()`
|
|
163
|
+
|
|
164
|
+
## Local commands
|
|
165
|
+
|
|
166
|
+
```shell
|
|
167
|
+
cd packages/provider-selection
|
|
168
|
+
pnpm build
|
|
169
|
+
pnpm test
|
|
170
|
+
pnpm coverage
|
|
171
|
+
pnpm typecheck
|
|
172
|
+
pnpm package:check
|
|
173
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
collectProvidersByCapability: () => collectProvidersByCapability,
|
|
24
|
+
resolveItemProviderSelection: () => resolveItemProviderSelection,
|
|
25
|
+
resolveProviderSelection: () => resolveProviderSelection
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
function toSortedUniqueProviderIds(providerIds) {
|
|
29
|
+
return Array.from(new Set(Array.from(providerIds).filter(Boolean))).sort();
|
|
30
|
+
}
|
|
31
|
+
function getSelectedProvider(input) {
|
|
32
|
+
const firstProviderId = input.candidateProviderIds[0];
|
|
33
|
+
if (!firstProviderId) {
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
const configuredProviderId = input.configuredProviders?.[input.capabilityId];
|
|
37
|
+
if (configuredProviderId) {
|
|
38
|
+
if (input.candidateProviderIds.includes(configuredProviderId)) {
|
|
39
|
+
return {
|
|
40
|
+
selectedProviderId: configuredProviderId,
|
|
41
|
+
mode: "configured"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
const fallbackProviderId = input.fallbackProviders?.[input.capabilityId];
|
|
47
|
+
if (fallbackProviderId && input.candidateProviderIds.includes(fallbackProviderId)) {
|
|
48
|
+
return {
|
|
49
|
+
selectedProviderId: fallbackProviderId,
|
|
50
|
+
mode: "fallback"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
selectedProviderId: firstProviderId,
|
|
55
|
+
mode: "first"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function selectProviders(input) {
|
|
59
|
+
const selections = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const [capabilityId, candidateProviderIds] of Array.from(
|
|
61
|
+
input.providersByCapability.entries()
|
|
62
|
+
).sort(([left], [right]) => left.localeCompare(right))) {
|
|
63
|
+
const normalizedCandidateProviderIds = toSortedUniqueProviderIds(candidateProviderIds);
|
|
64
|
+
const selected = getSelectedProvider({
|
|
65
|
+
capabilityId,
|
|
66
|
+
candidateProviderIds: normalizedCandidateProviderIds,
|
|
67
|
+
...input.configuredProviders ? { configuredProviders: input.configuredProviders } : {},
|
|
68
|
+
...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}
|
|
69
|
+
});
|
|
70
|
+
if (!selected) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
selections.set(capabilityId, {
|
|
74
|
+
capabilityId,
|
|
75
|
+
selectedProviderId: selected.selectedProviderId,
|
|
76
|
+
candidateProviderIds: normalizedCandidateProviderIds,
|
|
77
|
+
mode: selected.mode
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return selections;
|
|
81
|
+
}
|
|
82
|
+
function findConfiguredProviderMismatches(input) {
|
|
83
|
+
const mismatches = [];
|
|
84
|
+
for (const [capabilityId, configuredProviderId] of Object.entries(
|
|
85
|
+
input.configuredProviders ?? {}
|
|
86
|
+
).sort(([left], [right]) => left.localeCompare(right))) {
|
|
87
|
+
if (!configuredProviderId) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const candidateProviderIds = toSortedUniqueProviderIds(
|
|
91
|
+
input.providersByCapability.get(capabilityId) ?? []
|
|
92
|
+
);
|
|
93
|
+
if (!candidateProviderIds.length) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (candidateProviderIds.includes(configuredProviderId)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
mismatches.push({
|
|
100
|
+
capabilityId,
|
|
101
|
+
configuredProviderId
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return mismatches;
|
|
105
|
+
}
|
|
106
|
+
function getExcludedProviders(selections) {
|
|
107
|
+
const excludedProviderIds = [];
|
|
108
|
+
for (const selection of selections) {
|
|
109
|
+
if (selection.candidateProviderIds.length <= 1) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const candidateProviderId of selection.candidateProviderIds) {
|
|
113
|
+
if (candidateProviderId !== selection.selectedProviderId) {
|
|
114
|
+
excludedProviderIds.push(candidateProviderId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return toSortedUniqueProviderIds(excludedProviderIds);
|
|
119
|
+
}
|
|
120
|
+
function collectProvidersByCapability(input) {
|
|
121
|
+
const providersByCapability = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const item of input.items) {
|
|
123
|
+
const capabilityId = input.getCapabilityId(item);
|
|
124
|
+
if (!capabilityId) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const providerId = input.getProviderId(item);
|
|
128
|
+
const currentProviderIds = providersByCapability.get(capabilityId) ?? [];
|
|
129
|
+
currentProviderIds.push(providerId);
|
|
130
|
+
providersByCapability.set(capabilityId, toSortedUniqueProviderIds(currentProviderIds));
|
|
131
|
+
}
|
|
132
|
+
return providersByCapability;
|
|
133
|
+
}
|
|
134
|
+
function resolveProviderSelection(input) {
|
|
135
|
+
const selections = selectProviders(input);
|
|
136
|
+
const mismatches = findConfiguredProviderMismatches(input);
|
|
137
|
+
const excludedProviderIds = getExcludedProviders(selections.values());
|
|
138
|
+
return {
|
|
139
|
+
selections,
|
|
140
|
+
mismatches,
|
|
141
|
+
excludedProviderIds
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function resolveItemProviderSelection(input) {
|
|
145
|
+
const providersByCapability = collectProvidersByCapability(input);
|
|
146
|
+
const resolution = resolveProviderSelection({
|
|
147
|
+
providersByCapability,
|
|
148
|
+
...input.configuredProviders ? { configuredProviders: input.configuredProviders } : {},
|
|
149
|
+
...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
providersByCapability,
|
|
153
|
+
...resolution
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
157
|
+
0 && (module.exports = {
|
|
158
|
+
collectProvidersByCapability,
|
|
159
|
+
resolveItemProviderSelection,
|
|
160
|
+
resolveProviderSelection
|
|
161
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
type CapabilityId = string;
|
|
2
|
+
type ProviderId = string;
|
|
3
|
+
type ProviderSelectionMode = 'configured' | 'fallback' | 'first';
|
|
4
|
+
type ProviderPreferenceMap = Partial<Record<CapabilityId, ProviderId>>;
|
|
5
|
+
type ProviderSelection = {
|
|
6
|
+
capabilityId: CapabilityId;
|
|
7
|
+
selectedProviderId: ProviderId;
|
|
8
|
+
candidateProviderIds: ProviderId[];
|
|
9
|
+
mode: ProviderSelectionMode;
|
|
10
|
+
};
|
|
11
|
+
type ProviderMismatch = {
|
|
12
|
+
capabilityId: CapabilityId;
|
|
13
|
+
configuredProviderId: ProviderId;
|
|
14
|
+
};
|
|
15
|
+
type ProvidersByCapability = Map<CapabilityId, ProviderId[]>;
|
|
16
|
+
type ProviderSelectionResolution = {
|
|
17
|
+
selections: Map<CapabilityId, ProviderSelection>;
|
|
18
|
+
mismatches: ProviderMismatch[];
|
|
19
|
+
excludedProviderIds: ProviderId[];
|
|
20
|
+
};
|
|
21
|
+
type ItemProviderSelectionResolution = ProviderSelectionResolution & {
|
|
22
|
+
providersByCapability: ProvidersByCapability;
|
|
23
|
+
};
|
|
24
|
+
type ProviderCollectionInput<T> = {
|
|
25
|
+
items: Iterable<T>;
|
|
26
|
+
getCapabilityId: (item: T) => CapabilityId | undefined;
|
|
27
|
+
getProviderId: (item: T) => ProviderId;
|
|
28
|
+
};
|
|
29
|
+
type ResolveProviderSelectionInput = {
|
|
30
|
+
providersByCapability: ProvidersByCapability;
|
|
31
|
+
configuredProviders?: ProviderPreferenceMap;
|
|
32
|
+
fallbackProviders?: ProviderPreferenceMap;
|
|
33
|
+
};
|
|
34
|
+
type ResolveItemProviderSelectionInput<T> = ProviderCollectionInput<T> & {
|
|
35
|
+
configuredProviders?: ProviderPreferenceMap;
|
|
36
|
+
fallbackProviders?: ProviderPreferenceMap;
|
|
37
|
+
};
|
|
38
|
+
declare function collectProvidersByCapability<T>(input: ProviderCollectionInput<T>): ProvidersByCapability;
|
|
39
|
+
declare function resolveProviderSelection(input: ResolveProviderSelectionInput): ProviderSelectionResolution;
|
|
40
|
+
declare function resolveItemProviderSelection<T>(input: ResolveItemProviderSelectionInput<T>): ItemProviderSelectionResolution;
|
|
41
|
+
|
|
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 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
type CapabilityId = string;
|
|
2
|
+
type ProviderId = string;
|
|
3
|
+
type ProviderSelectionMode = 'configured' | 'fallback' | 'first';
|
|
4
|
+
type ProviderPreferenceMap = Partial<Record<CapabilityId, ProviderId>>;
|
|
5
|
+
type ProviderSelection = {
|
|
6
|
+
capabilityId: CapabilityId;
|
|
7
|
+
selectedProviderId: ProviderId;
|
|
8
|
+
candidateProviderIds: ProviderId[];
|
|
9
|
+
mode: ProviderSelectionMode;
|
|
10
|
+
};
|
|
11
|
+
type ProviderMismatch = {
|
|
12
|
+
capabilityId: CapabilityId;
|
|
13
|
+
configuredProviderId: ProviderId;
|
|
14
|
+
};
|
|
15
|
+
type ProvidersByCapability = Map<CapabilityId, ProviderId[]>;
|
|
16
|
+
type ProviderSelectionResolution = {
|
|
17
|
+
selections: Map<CapabilityId, ProviderSelection>;
|
|
18
|
+
mismatches: ProviderMismatch[];
|
|
19
|
+
excludedProviderIds: ProviderId[];
|
|
20
|
+
};
|
|
21
|
+
type ItemProviderSelectionResolution = ProviderSelectionResolution & {
|
|
22
|
+
providersByCapability: ProvidersByCapability;
|
|
23
|
+
};
|
|
24
|
+
type ProviderCollectionInput<T> = {
|
|
25
|
+
items: Iterable<T>;
|
|
26
|
+
getCapabilityId: (item: T) => CapabilityId | undefined;
|
|
27
|
+
getProviderId: (item: T) => ProviderId;
|
|
28
|
+
};
|
|
29
|
+
type ResolveProviderSelectionInput = {
|
|
30
|
+
providersByCapability: ProvidersByCapability;
|
|
31
|
+
configuredProviders?: ProviderPreferenceMap;
|
|
32
|
+
fallbackProviders?: ProviderPreferenceMap;
|
|
33
|
+
};
|
|
34
|
+
type ResolveItemProviderSelectionInput<T> = ProviderCollectionInput<T> & {
|
|
35
|
+
configuredProviders?: ProviderPreferenceMap;
|
|
36
|
+
fallbackProviders?: ProviderPreferenceMap;
|
|
37
|
+
};
|
|
38
|
+
declare function collectProvidersByCapability<T>(input: ProviderCollectionInput<T>): ProvidersByCapability;
|
|
39
|
+
declare function resolveProviderSelection(input: ResolveProviderSelectionInput): ProviderSelectionResolution;
|
|
40
|
+
declare function resolveItemProviderSelection<T>(input: ResolveItemProviderSelectionInput<T>): ItemProviderSelectionResolution;
|
|
41
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function toSortedUniqueProviderIds(providerIds) {
|
|
3
|
+
return Array.from(new Set(Array.from(providerIds).filter(Boolean))).sort();
|
|
4
|
+
}
|
|
5
|
+
function getSelectedProvider(input) {
|
|
6
|
+
const firstProviderId = input.candidateProviderIds[0];
|
|
7
|
+
if (!firstProviderId) {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
const configuredProviderId = input.configuredProviders?.[input.capabilityId];
|
|
11
|
+
if (configuredProviderId) {
|
|
12
|
+
if (input.candidateProviderIds.includes(configuredProviderId)) {
|
|
13
|
+
return {
|
|
14
|
+
selectedProviderId: configuredProviderId,
|
|
15
|
+
mode: "configured"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
const fallbackProviderId = input.fallbackProviders?.[input.capabilityId];
|
|
21
|
+
if (fallbackProviderId && input.candidateProviderIds.includes(fallbackProviderId)) {
|
|
22
|
+
return {
|
|
23
|
+
selectedProviderId: fallbackProviderId,
|
|
24
|
+
mode: "fallback"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
selectedProviderId: firstProviderId,
|
|
29
|
+
mode: "first"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function selectProviders(input) {
|
|
33
|
+
const selections = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const [capabilityId, candidateProviderIds] of Array.from(
|
|
35
|
+
input.providersByCapability.entries()
|
|
36
|
+
).sort(([left], [right]) => left.localeCompare(right))) {
|
|
37
|
+
const normalizedCandidateProviderIds = toSortedUniqueProviderIds(candidateProviderIds);
|
|
38
|
+
const selected = getSelectedProvider({
|
|
39
|
+
capabilityId,
|
|
40
|
+
candidateProviderIds: normalizedCandidateProviderIds,
|
|
41
|
+
...input.configuredProviders ? { configuredProviders: input.configuredProviders } : {},
|
|
42
|
+
...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}
|
|
43
|
+
});
|
|
44
|
+
if (!selected) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
selections.set(capabilityId, {
|
|
48
|
+
capabilityId,
|
|
49
|
+
selectedProviderId: selected.selectedProviderId,
|
|
50
|
+
candidateProviderIds: normalizedCandidateProviderIds,
|
|
51
|
+
mode: selected.mode
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return selections;
|
|
55
|
+
}
|
|
56
|
+
function findConfiguredProviderMismatches(input) {
|
|
57
|
+
const mismatches = [];
|
|
58
|
+
for (const [capabilityId, configuredProviderId] of Object.entries(
|
|
59
|
+
input.configuredProviders ?? {}
|
|
60
|
+
).sort(([left], [right]) => left.localeCompare(right))) {
|
|
61
|
+
if (!configuredProviderId) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const candidateProviderIds = toSortedUniqueProviderIds(
|
|
65
|
+
input.providersByCapability.get(capabilityId) ?? []
|
|
66
|
+
);
|
|
67
|
+
if (!candidateProviderIds.length) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (candidateProviderIds.includes(configuredProviderId)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
mismatches.push({
|
|
74
|
+
capabilityId,
|
|
75
|
+
configuredProviderId
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return mismatches;
|
|
79
|
+
}
|
|
80
|
+
function getExcludedProviders(selections) {
|
|
81
|
+
const excludedProviderIds = [];
|
|
82
|
+
for (const selection of selections) {
|
|
83
|
+
if (selection.candidateProviderIds.length <= 1) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (const candidateProviderId of selection.candidateProviderIds) {
|
|
87
|
+
if (candidateProviderId !== selection.selectedProviderId) {
|
|
88
|
+
excludedProviderIds.push(candidateProviderId);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return toSortedUniqueProviderIds(excludedProviderIds);
|
|
93
|
+
}
|
|
94
|
+
function collectProvidersByCapability(input) {
|
|
95
|
+
const providersByCapability = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const item of input.items) {
|
|
97
|
+
const capabilityId = input.getCapabilityId(item);
|
|
98
|
+
if (!capabilityId) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const providerId = input.getProviderId(item);
|
|
102
|
+
const currentProviderIds = providersByCapability.get(capabilityId) ?? [];
|
|
103
|
+
currentProviderIds.push(providerId);
|
|
104
|
+
providersByCapability.set(capabilityId, toSortedUniqueProviderIds(currentProviderIds));
|
|
105
|
+
}
|
|
106
|
+
return providersByCapability;
|
|
107
|
+
}
|
|
108
|
+
function resolveProviderSelection(input) {
|
|
109
|
+
const selections = selectProviders(input);
|
|
110
|
+
const mismatches = findConfiguredProviderMismatches(input);
|
|
111
|
+
const excludedProviderIds = getExcludedProviders(selections.values());
|
|
112
|
+
return {
|
|
113
|
+
selections,
|
|
114
|
+
mismatches,
|
|
115
|
+
excludedProviderIds
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function resolveItemProviderSelection(input) {
|
|
119
|
+
const providersByCapability = collectProvidersByCapability(input);
|
|
120
|
+
const resolution = resolveProviderSelection({
|
|
121
|
+
providersByCapability,
|
|
122
|
+
...input.configuredProviders ? { configuredProviders: input.configuredProviders } : {},
|
|
123
|
+
...input.fallbackProviders ? { fallbackProviders: input.fallbackProviders } : {}
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
providersByCapability,
|
|
127
|
+
...resolution
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
collectProvidersByCapability,
|
|
132
|
+
resolveItemProviderSelection,
|
|
133
|
+
resolveProviderSelection
|
|
134
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
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']
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
// []
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lorion-org/provider-selection",
|
|
3
|
+
"version": "1.0.0-beta.0",
|
|
4
|
+
"description": "Framework-free capability provider selection primitives.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/lorion-org/lorion.git",
|
|
9
|
+
"directory": "packages/provider-selection"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/lorion-org/lorion/tree/main/packages/provider-selection#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/lorion-org/lorion/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"require": {
|
|
30
|
+
"types": "./dist/index.d.cts",
|
|
31
|
+
"default": "./dist/index.cjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"examples",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"keywords": [
|
|
41
|
+
"provider",
|
|
42
|
+
"selection",
|
|
43
|
+
"capability",
|
|
44
|
+
"typescript"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
51
|
+
"clean": "rimraf coverage dist tsconfig.tsbuildinfo",
|
|
52
|
+
"lint": "eslint src --ext .ts",
|
|
53
|
+
"test": "vitest run --root ../.. --config vitest.config.mts packages/provider-selection/src/index.spec.ts",
|
|
54
|
+
"coverage": "node -e \"require('node:fs').mkdirSync('../../coverage/.tmp', { recursive: true })\" && vitest run --coverage --coverage.include=packages/provider-selection/src/**/*.ts --root ../.. --config vitest.config.mts packages/provider-selection/src/index.spec.ts",
|
|
55
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
56
|
+
"package:check": "pnpm build && pnpm pack --dry-run && publint"
|
|
57
|
+
}
|
|
58
|
+
}
|