@tbox.cn/app-toolkit 0.2.0 → 0.3.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/dist/index.d.ts +91 -25
- package/dist/index.js +8 -8
- package/package.json +2 -2
- package/src/assembly/app-manifest.ts +6 -0
- package/src/dto.ts +46 -4
- package/src/factory.ts +58 -16
- package/src/index.ts +4 -2
- package/src/integrations/credentials.ts +1 -1
- package/src/integrations/predicate.ts +2 -1
- package/src/integrations/write.ts +15 -11
- package/src/views/integration-schemas.ts +197 -24
- package/src/views/modules.ts +18 -1
- package/src/views/providers.ts +26 -20
- package/tests/file-cache.test.ts +3 -1
- package/tests/views.test.ts +940 -72
- package/tests/write-core.test.ts +26 -4
package/src/dto.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DTO 全量(api.md §3 字段级单源落源——
|
|
2
|
+
* DTO 全量(api.md §3 字段级单源落源——36 类型;演进 additive-only,铁律 #8)。
|
|
3
3
|
* agtcodingbox / tbox-ai-coding 经 `import type` 消费(L2 DTO 单源——API 层零类型镜像)。
|
|
4
4
|
* 词汇七概念:app / module / service / instance / provider / integration / resolution。
|
|
5
5
|
* 弃用词汇清单见 api.md §3 尾(component/slot/scope/configMode/…——防复活)。
|
|
@@ -20,13 +20,15 @@ export type BindingLayer = 'app' | 'module' | 'service' | 'instance';
|
|
|
20
20
|
|
|
21
21
|
// ===== 需求轴(GET /modules · 静态)=====
|
|
22
22
|
|
|
23
|
-
/** 纯模块清单(无 services——卡片展开走 /modules/:m
|
|
23
|
+
/** 纯模块清单(无 services——卡片展开走 /modules/:m);provider 家族包不在清单
|
|
24
|
+
* (供给/实施载体非可配置业务模块——供给轴归 /providers;判定 = install id / 包名
|
|
25
|
+
* basename 去 scope 后 `provider-` 前缀;已装 provider 详情 GET /modules/:m 仍 200) */
|
|
24
26
|
export interface ModulesView {
|
|
25
27
|
modules: ModuleDeclaration[];
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface ModuleDeclaration {
|
|
29
|
-
/** install id(packages/ 目录名 / .tbox/app.json entry.id
|
|
31
|
+
/** install id(packages/ 目录名 / .tbox/app.json entry.id)——恒业务模块(provider 家族不在本清单) */
|
|
30
32
|
id: string;
|
|
31
33
|
/** 包名 */
|
|
32
34
|
name: string;
|
|
@@ -62,6 +64,9 @@ export interface ModuleDetailView {
|
|
|
62
64
|
serviceResolutions?: ServiceResolutionsView | null;
|
|
63
65
|
/** v4.4 D32:include=integrationSchemas 层级富化(多域/零域/共享键/未绑定 → null) */
|
|
64
66
|
integrationSchemas?: LayerIntegrationSchemas | null;
|
|
67
|
+
/** v4.6 D36:include=integrationFieldRegistry 集成字段注册表(null 语义镜像 integrationSchemas;
|
|
68
|
+
* 未知厂商/零足迹 → 非 null 空集 + unsupplied 回填——D37 轴参假想计算同此) */
|
|
69
|
+
integrationFieldRegistry?: IntegrationFieldRegistryView | null;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/** 模块域厂商绑定解析(v4.4 D34——静态文件事实;规则 ≡ 谓词 #7 域级,resolveDomainBinding 单源) */
|
|
@@ -82,6 +87,27 @@ export interface LayerIntegrationSchemas {
|
|
|
82
87
|
services: Record<string, { configSchema: object | null }>;
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/** v4.6 D36:集成字段注册表(层级表单服务端预计算——合并代数 R1/R2/R3 服务端单源;
|
|
91
|
+
* 消费 resolveLayerIntegrationSchemas 产物构造,禁旁路直取 serviceSchemas) */
|
|
92
|
+
export interface IntegrationFieldRegistryView {
|
|
93
|
+
/** echo:本注册表计算口径(假想时 = 轴参值——D37) */
|
|
94
|
+
provider: string;
|
|
95
|
+
/** 作用域内 configSchema === null 的服务(UI「未提供参数模板」提示行) */
|
|
96
|
+
schemaless: string[];
|
|
97
|
+
/** module 轴 = demand − 该厂商供给(Set 去重保序);app 轴恒 [](结构性:足迹 ⊆ 供给) */
|
|
98
|
+
unsupplied: string[];
|
|
99
|
+
/** 作用域内全部非 null schema 的 additionalProperties 均 ≠ false(聚合;空真) */
|
|
100
|
+
additionalPropertiesAllowed: boolean;
|
|
101
|
+
/** 逐 key 合并产物(首声明插入序 = scope 遍历序:app 字母序 / module demand 序) */
|
|
102
|
+
fields: Array<{
|
|
103
|
+
key: string;
|
|
104
|
+
/** null = 类型不可调和(R3/R2 异构/R1 enum 不等——冲突折叠,客户端 RawJson 保真) */
|
|
105
|
+
schema: object | null;
|
|
106
|
+
/** required 聚合 = sources.some(s => s.required);conflict 字段照常聚合 */
|
|
107
|
+
sources: Array<{ service: string; required: boolean }>;
|
|
108
|
+
}>;
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
/** GET /app(应用静态视图:平台服务 + root 原值) */
|
|
86
112
|
export interface AppView {
|
|
87
113
|
/** 平台服务(v4.4 D29:服务词汇 − 模块占用——与模块 demand 同型 ServiceDemand[];
|
|
@@ -93,6 +119,9 @@ export interface AppView {
|
|
|
93
119
|
serviceResolutions?: ServiceResolutionsView | null;
|
|
94
120
|
/** v4.4 D32:include=integrationSchemas 层级富化(root.provider 缺席 → null) */
|
|
95
121
|
integrationSchemas?: LayerIntegrationSchemas | null;
|
|
122
|
+
/** v4.6 D36:include=integrationFieldRegistry 集成字段注册表(root.provider 缺席且无轴参 → null;
|
|
123
|
+
* 未知厂商/零足迹 → 非 null 空集——D37 轴参假想计算同此) */
|
|
124
|
+
integrationFieldRegistry?: IntegrationFieldRegistryView | null;
|
|
96
125
|
}
|
|
97
126
|
|
|
98
127
|
/** GET /services/:service(服务静态单体) */
|
|
@@ -103,6 +132,18 @@ export interface ServiceDetail extends ServiceDemand {
|
|
|
103
132
|
integration: ServiceIntegrationNode | null;
|
|
104
133
|
/** 供给该服务的厂商名单(catalog 静态切片——picker 数据源) */
|
|
105
134
|
suppliedBy: string[];
|
|
135
|
+
/** v4.7 D38:include=integrationSchemas 生效绑定富化(三态:null 全链未绑定 /
|
|
136
|
+
* 绑定无可计算 schema / 含 schema;malformed 节点 → null——不放行域级联) */
|
|
137
|
+
integrationSchemas?: ServiceIntegrationSchemas | null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 服务级生效绑定 schema(v4.7 D38 静态轴富化——零 contracts,永不 503;实例层无条件排除:
|
|
141
|
+
* node 级口径 ≠ resolution 默认实例口径,api.md §4.4 注) */
|
|
142
|
+
export interface ServiceIntegrationSchemas {
|
|
143
|
+
/** 生效厂商:显式(含内联声明名)?? domains[首段].provider ?? root.provider */
|
|
144
|
+
provider: string;
|
|
145
|
+
/** catalog 复合键(多 impl 声明序首个)> 内联 ref;miss(未知厂商/未供给/内联缺 ref)→ null(富化非门控) */
|
|
146
|
+
configSchema: object | null;
|
|
106
147
|
}
|
|
107
148
|
|
|
108
149
|
// ===== 求值轴 =====
|
|
@@ -359,7 +400,8 @@ export interface SaveResult {
|
|
|
359
400
|
written: boolean;
|
|
360
401
|
/** 恒 false 当 dryRun / no-diff / 应用未运行(onApplied 协作点回填) */
|
|
361
402
|
restartScheduled: boolean;
|
|
362
|
-
/**
|
|
403
|
+
/** 触碰清单;dryRun = 将触碰预览(D35——与同 body 真实写逐字对称,含凭据工件路径;
|
|
404
|
+
* no-diff 早退 = 预览零触碰) */
|
|
363
405
|
files: string[];
|
|
364
406
|
/** 凭据工件(0..n;dryRun = 将创建的工件——确定性 stem 派生预览) */
|
|
365
407
|
credentials: Array<{ stem: string; ref: string; overwrote: boolean }>;
|
package/src/factory.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { loadModulesView, loadModuleDetailView } from './views/modules.js';
|
|
|
19
19
|
import { loadAppView } from './views/app.js';
|
|
20
20
|
import { loadServiceDetailView } from './views/service-detail.js';
|
|
21
21
|
import { loadServiceResolutionsView, loadServiceResolutionDetail } from './views/service-resolutions.js';
|
|
22
|
-
import {
|
|
22
|
+
import { loadAppFormViews, loadModuleFormViews, loadServiceIntegrationSchemas } from './views/integration-schemas.js';
|
|
23
23
|
import { loadCredentialsView } from './views/credentials.js';
|
|
24
24
|
import { loadCredentialTypesView } from './views/credential-types.js';
|
|
25
25
|
import type { WalkManifestsResult } from './assembly/walk-manifests.js';
|
|
@@ -42,7 +42,9 @@ import type {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* createAppToolkit(C4 工厂;v4.4——24 方法 = 11 读(含 credentials/credential-types 双端点)
|
|
45
|
-
* + 4 写 + 9 迁移面;loadApp/loadModule/loadProvider 增 include
|
|
45
|
+
* + 4 写 + 9 迁移面;loadApp/loadModule/loadProvider 增 include 选项;v4.6——loadApp/loadModule
|
|
46
|
+
* += 轴向假想绑定选项(appProvider/moduleProvider,D37);表单双投影经 formViews 单点装配;
|
|
47
|
+
* v4.7——loadService 增 include 选项(服务级生效绑定 schema 单体))。
|
|
46
48
|
* AppToolkit = AppToolkitRead & AppToolkitWrite & AppToolkitInternals——
|
|
47
49
|
* Read+Write = HTTP 直通面 additive 冻结(L1 端点↔方法 1:1,L3 命名镜像:Integration=节点级 /
|
|
48
50
|
* Config=文件级);Internals = CLI/doctor/桥消费(自由演进)。
|
|
@@ -63,11 +65,14 @@ export interface AppToolkitOptions {
|
|
|
63
65
|
onApplied?: () => void;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
/** include 协议白名单(v4.4 D30/P15——键名 = 产出字段名 1:1;per 端点;注册表 additive-only
|
|
68
|
+
/** include 协议白名单(v4.4 D30/P15——键名 = 产出字段名 1:1;per 端点;注册表 additive-only;
|
|
69
|
+
* v4.6 D36 += integrationFieldRegistry:app/module 三键,provider 轴不加——YAGNI;
|
|
70
|
+
* v4.7 D38 += service 键——服务级生效绑定 schema) */
|
|
67
71
|
const INCLUDE_WHITELIST = {
|
|
68
|
-
app: ['serviceResolutions', 'integrationSchemas'],
|
|
69
|
-
module: ['serviceResolutions', 'integrationSchemas'],
|
|
72
|
+
app: ['serviceResolutions', 'integrationSchemas', 'integrationFieldRegistry'],
|
|
73
|
+
module: ['serviceResolutions', 'integrationSchemas', 'integrationFieldRegistry'],
|
|
70
74
|
provider: ['integrationSchemas'],
|
|
75
|
+
service: ['integrationSchemas'],
|
|
71
76
|
} as const;
|
|
72
77
|
|
|
73
78
|
/** include 校验(P15 单点在 toolkit 方法内——壳层仅解析可重复参数,零校验纪律不变):
|
|
@@ -87,12 +92,35 @@ function validateInclude(kind: keyof typeof INCLUDE_WHITELIST, include?: string[
|
|
|
87
92
|
return new Set(include);
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
/** 表单层 include 键(D37 轴参严检依据——serviceResolutions 不算) */
|
|
96
|
+
const FORM_INCLUDE_KEYS = ['integrationSchemas', 'integrationFieldRegistry'] as const;
|
|
97
|
+
|
|
98
|
+
/** v4.6 D37 轴参严检(400 先于 404——镜像输入校验先行):非空轴参需与表单 include 键同现;
|
|
99
|
+
* 缺席/空串 ≡ 按实际配置解析(I3——不触发严检);错轴参数由壳层按名读取,天然静默忽略。 */
|
|
100
|
+
function assertAxisProviderUsable(
|
|
101
|
+
paramName: 'appProvider' | 'moduleProvider',
|
|
102
|
+
value: string | undefined,
|
|
103
|
+
include: ReadonlySet<string>,
|
|
104
|
+
): void {
|
|
105
|
+
if (typeof value !== 'string' || value.length === 0) return;
|
|
106
|
+
if (!FORM_INCLUDE_KEYS.some((k) => include.has(k))) {
|
|
107
|
+
throw new AppToolkitError(
|
|
108
|
+
'VALIDATION_FAILED',
|
|
109
|
+
400,
|
|
110
|
+
`参数 ${paramName} 需与表单 include 键(${FORM_INCLUDE_KEYS.join('、')})同现——轴参仅重定向表单层双投影`,
|
|
111
|
+
[paramName],
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
90
116
|
/** ── HTTP 直通面(11 读;L1 镜像律:方法名 = load + URL 资源段;query → 选项字段 L4)── */
|
|
91
117
|
export interface AppToolkitRead {
|
|
92
118
|
loadModules(): Promise<ModulesView>;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
/** v4.6 D37:moduleProvider = 轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include 键同现) */
|
|
120
|
+
loadModule(moduleId: string, o?: { include?: string[]; moduleProvider?: string }): Promise<ModuleDetailView>;
|
|
121
|
+
/** v4.6 D37:appProvider = 轴向假想绑定(缺席/空串 ≡ 按实际配置解析;需与表单 include 键同现) */
|
|
122
|
+
loadApp(o?: { include?: string[]; appProvider?: string }): Promise<AppView>;
|
|
123
|
+
loadService(service: string, o?: { include?: string[] }): Promise<ServiceDetail>;
|
|
96
124
|
loadServiceResolutions(services?: string[]): Promise<ServiceResolutionsView>;
|
|
97
125
|
loadServiceResolution(service: string): Promise<ServiceResolutionDetail>;
|
|
98
126
|
loadProviders(): Promise<ProvidersView>;
|
|
@@ -201,8 +229,9 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
|
|
|
201
229
|
// v4.4 D31:门控与内嵌分离——静态方法零门控分支;include=serviceResolutions 按
|
|
202
230
|
// evaluation && strictValidation 双条件装配(<0.9/形态异常 → null,永不 503——技术方案 §2.4:
|
|
203
231
|
// 仅判 strictValidation 会被「严格面在场+求值面缺席」畸形包击穿)
|
|
204
|
-
async loadModule(moduleId: string, o?: { include?: string[] }): Promise<ModuleDetailView> {
|
|
232
|
+
async loadModule(moduleId: string, o?: { include?: string[]; moduleProvider?: string }): Promise<ModuleDetailView> {
|
|
205
233
|
const include = validateInclude('module', o?.include); // ① 输入校验先行(400 先于 404)
|
|
234
|
+
assertAxisProviderUsable('moduleProvider', o?.moduleProvider, include); // ①' D37 轴参严检(先于 404)
|
|
206
235
|
const contracts = await resolveContractsForApp(appDir);
|
|
207
236
|
const snap = snapshot(contracts);
|
|
208
237
|
const view = loadModuleDetailView(snap, moduleId); // ② MODULE_NOT_FOUND
|
|
@@ -212,13 +241,17 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
|
|
|
212
241
|
loadServiceResolutionsView(snap, issues, view.services.map((s) => s.service)))
|
|
213
242
|
: null; // module = demand 服务过滤(≡ 批量 ?service= 语义;issues 恒全量)
|
|
214
243
|
}
|
|
215
|
-
if (include.has('integrationSchemas')) {
|
|
216
|
-
|
|
244
|
+
if (include.has('integrationSchemas') || include.has('integrationFieldRegistry')) {
|
|
245
|
+
// v4.6 D37:表单层双投影同源一次计算(双取单算 + I1——假想重定向天然同步)
|
|
246
|
+
const form = loadModuleFormViews(snap, moduleId, o?.moduleProvider);
|
|
247
|
+
if (include.has('integrationSchemas')) view.integrationSchemas = form.integrationSchemas;
|
|
248
|
+
if (include.has('integrationFieldRegistry')) view.integrationFieldRegistry = form.integrationFieldRegistry;
|
|
217
249
|
}
|
|
218
250
|
return view;
|
|
219
251
|
},
|
|
220
|
-
async loadApp(o?: { include?: string[] }): Promise<AppView> {
|
|
252
|
+
async loadApp(o?: { include?: string[]; appProvider?: string }): Promise<AppView> {
|
|
221
253
|
const include = validateInclude('app', o?.include);
|
|
254
|
+
assertAxisProviderUsable('appProvider', o?.appProvider, include); // D37 轴参严检(先于任何计算)
|
|
222
255
|
const contracts = await resolveContractsForApp(appDir);
|
|
223
256
|
const snap = snapshot(contracts);
|
|
224
257
|
const view = loadAppView(snap);
|
|
@@ -227,14 +260,23 @@ export function createAppToolkit(appDir: string, options: AppToolkitOptions = {}
|
|
|
227
260
|
? await predicateIssues().then((issues) => loadServiceResolutionsView(snap, issues)) // app = 全量键集
|
|
228
261
|
: null;
|
|
229
262
|
}
|
|
230
|
-
if (include.has('integrationSchemas')) {
|
|
231
|
-
|
|
263
|
+
if (include.has('integrationSchemas') || include.has('integrationFieldRegistry')) {
|
|
264
|
+
const form = loadAppFormViews(snap, o?.appProvider); // 双取单算 + I1(同 loadModule)
|
|
265
|
+
if (include.has('integrationSchemas')) view.integrationSchemas = form.integrationSchemas;
|
|
266
|
+
if (include.has('integrationFieldRegistry')) view.integrationFieldRegistry = form.integrationFieldRegistry;
|
|
232
267
|
}
|
|
233
268
|
return view;
|
|
234
269
|
},
|
|
235
|
-
|
|
270
|
+
// v4.7 D38:include 镜像 loadModule 三段式——① 输入校验 400 先于 404 ② 视图 ③ 静态富化(永不 503)
|
|
271
|
+
async loadService(service: string, o?: { include?: string[] }): Promise<ServiceDetail> {
|
|
272
|
+
const include = validateInclude('service', o?.include);
|
|
236
273
|
const contracts = await resolveContractsForApp(appDir);
|
|
237
|
-
|
|
274
|
+
const snap = snapshot(contracts); // 单快照——detail 与 include 装配共享
|
|
275
|
+
const view = loadServiceDetailView(snap, service);
|
|
276
|
+
if (include.has('integrationSchemas')) {
|
|
277
|
+
view.integrationSchemas = loadServiceIntegrationSchemas(snap, service);
|
|
278
|
+
}
|
|
279
|
+
return view;
|
|
238
280
|
},
|
|
239
281
|
async loadServiceResolutions(services?: string[]) {
|
|
240
282
|
const contracts = await resolveContractsForApp(appDir);
|
package/src/index.ts
CHANGED
|
@@ -85,7 +85,7 @@ export { loadAppView } from './views/app.js';
|
|
|
85
85
|
export { loadServiceDetailView } from './views/service-detail.js';
|
|
86
86
|
export { loadProvidersView, loadProviderDetailView, loadProviderServiceDetailView } from './views/providers.js';
|
|
87
87
|
export { loadServiceResolutionsView, loadServiceResolutionDetail, resolveServiceSummary } from './views/service-resolutions.js';
|
|
88
|
-
export { loadAppIntegrationSchemas, loadModuleIntegrationSchemas, resolveLayerIntegrationSchemas } from './views/integration-schemas.js';
|
|
88
|
+
export { loadAppIntegrationSchemas, loadModuleIntegrationSchemas, loadServiceIntegrationSchemas, resolveLayerIntegrationSchemas } from './views/integration-schemas.js';
|
|
89
89
|
export { echoForRef, loadCredentialsView } from './views/credentials.js';
|
|
90
90
|
export { loadCredentialTypesView } from './views/credential-types.js';
|
|
91
91
|
export type { ViewContext, ViewSnapshot } from './views/context.js';
|
|
@@ -94,7 +94,7 @@ export type { ViewContext, ViewSnapshot } from './views/context.js';
|
|
|
94
94
|
export { createAppToolkit } from './factory.js';
|
|
95
95
|
export type { AppToolkit, AppToolkitRead, AppToolkitWrite, AppToolkitInternals, AppToolkitOptions } from './factory.js';
|
|
96
96
|
|
|
97
|
-
// ── DTO(api.md §3 字段级单源——
|
|
97
|
+
// ── DTO(api.md §3 字段级单源——36 类型;agtcodingbox/tbox-ai-coding import type 消费)──
|
|
98
98
|
export type {
|
|
99
99
|
ServiceStatus,
|
|
100
100
|
BindingLayer,
|
|
@@ -104,8 +104,10 @@ export type {
|
|
|
104
104
|
ModuleDetailView,
|
|
105
105
|
AppView,
|
|
106
106
|
ServiceDetail,
|
|
107
|
+
ServiceIntegrationSchemas,
|
|
107
108
|
ProviderBinding,
|
|
108
109
|
LayerIntegrationSchemas,
|
|
110
|
+
IntegrationFieldRegistryView,
|
|
109
111
|
CredentialsView,
|
|
110
112
|
CredentialBindingEcho,
|
|
111
113
|
CredentialTypesView,
|
|
@@ -141,7 +141,7 @@ export function reconcileOrphanCredentials(appDir: string): OrphanIssue[] {
|
|
|
141
141
|
issues.push({
|
|
142
142
|
severity: 'warning',
|
|
143
143
|
code: 'ORPHANED_CREDENTIAL',
|
|
144
|
-
message:
|
|
144
|
+
message: `${stemToFilePath(stem)} 不再被引用(凭据文件保留,可手动清理)`,
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
return issues;
|
|
@@ -481,7 +481,8 @@ export function evaluateIntegrationServices(
|
|
|
481
481
|
// 不应被 B 厂商判死。引用面(v3)= root.provider ∪ domains.*.provider ∪ 槽级(含内联
|
|
482
482
|
// .provider)——terse 化后槽级双字段退场,引用面不上移即静默漏检(N2)。
|
|
483
483
|
// 保守跳过条件(无法确证即不判死——误报会阻断部署):任一引用厂商 configSchema 文件缺失,
|
|
484
|
-
// 或被绑定但零 configSchema 声明(如
|
|
484
|
+
// 或被绑定但零 configSchema 声明(如 local 供给未声明 configSchema——无 schema 知识。
|
|
485
|
+
// 注:provider-mock 0.3.0 起全槽带 schema,mock 绑定下本检查已生效)。
|
|
485
486
|
if (vendorConfigKeys) {
|
|
486
487
|
const referenced = new Set<string>();
|
|
487
488
|
let schemaIncomplete = false;
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import Ajv from 'ajv';
|
|
4
4
|
import { resolveContractsForApp } from '../core/contracts-resolver.js';
|
|
5
|
+
import { stemToFilePath } from '../core/credential-format.js';
|
|
5
6
|
import type { FileCache } from '../core/file-cache.js';
|
|
6
7
|
import { writeAtomic } from '../core/fskit.js';
|
|
7
8
|
import { SAFE_NAME_PATTERN } from '../core/module-schema.js';
|
|
@@ -36,8 +37,9 @@ import { AppToolkitError } from '../core/errors.js';
|
|
|
36
37
|
* 7 严格整文档校验(contracts parseIntegrationsConfig——触发 = !noDiff ∥ 有凭据输入)
|
|
37
38
|
* 8 凭据工件落盘(**后置于校验通过**——防「凭据已写 + zod 拒绝」不一致态;
|
|
38
39
|
* dryRun → writeCredentialArtifacts 零写盘 + existsSync 预览)
|
|
39
|
-
* 9 finalize
|
|
40
|
-
* restartScheduled = written && onApplied(凭据影响 boot
|
|
40
|
+
* 9 finalize(D35):planned = 将触碰(files 预览单源——dryRun 响应 ≡ 同 body 真实写,files 逐字对称);
|
|
41
|
+
* written = 实写(!dryRun 派生,D-B2);restartScheduled = written && onApplied(凭据影响 boot 装配——
|
|
42
|
+
* 重启正当);写后缓存失效
|
|
41
43
|
* 归一化:N1 模块节点 {} → 删 domains[键];N3 服务节点 {} 不归一化(显式通配激活 = 合法 TIER0 态);
|
|
42
44
|
* N2 已随 ?instance= 退役。dryRun(PUT only):复读完整管线不落盘不排程。
|
|
43
45
|
* DELETE:节点移除 + 孤儿凭据写后对账(覆盖 DELETE 与 PUT 替换两类来源,只报告不清理——D9)。
|
|
@@ -245,11 +247,13 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
245
247
|
appliedCredentials: AppliedCredential[],
|
|
246
248
|
issues: IntegrationServiceIssue[],
|
|
247
249
|
dryRun: boolean | undefined,
|
|
248
|
-
credentialsWritten: boolean,
|
|
249
250
|
): SaveResult {
|
|
250
|
-
//
|
|
251
|
-
// restartScheduled 跟随 written(凭据文件影响 boot 装配——重启正当)
|
|
252
|
-
const
|
|
251
|
+
// D35 双轴派生:planned = 将触碰(files 预览单源——dryRun 响应 ≡ 同 body 真实写,files 逐字对称);
|
|
252
|
+
// written = 实际落盘(!dryRun 派生——D-B2);restartScheduled 跟随 written(凭据文件影响 boot 装配——重启正当)
|
|
253
|
+
const integrationsPlanned = next !== null;
|
|
254
|
+
const credentialsPlanned = appliedCredentials.length > 0;
|
|
255
|
+
const integrationsWritten = !dryRun && integrationsPlanned;
|
|
256
|
+
const credentialsWritten = !dryRun && credentialsPlanned;
|
|
253
257
|
if (integrationsWritten) {
|
|
254
258
|
writeAtomic(join(appDir, INTEGRATIONS_FILE), `${JSON.stringify(next, null, 2)}\n`);
|
|
255
259
|
cache?.invalidate([join(appDir, INTEGRATIONS_FILE)]);
|
|
@@ -258,8 +262,8 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
258
262
|
const restartScheduled = written && onApplied !== undefined;
|
|
259
263
|
if (restartScheduled) onApplied?.();
|
|
260
264
|
const files: string[] = [];
|
|
261
|
-
if (
|
|
262
|
-
if (
|
|
265
|
+
if (integrationsPlanned) files.push(INTEGRATIONS_FILE);
|
|
266
|
+
if (credentialsPlanned) files.push(...appliedCredentials.map((a) => stemToFilePath(a.stem)));
|
|
263
267
|
return {
|
|
264
268
|
written,
|
|
265
269
|
restartScheduled,
|
|
@@ -361,7 +365,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
361
365
|
const dryRun = writeOpts.dryRun === true;
|
|
362
366
|
const applied = writeCredentialArtifacts(appDir, plan, dryRun);
|
|
363
367
|
issues.push(...registryCheck(next));
|
|
364
|
-
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun
|
|
368
|
+
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun);
|
|
365
369
|
},
|
|
366
370
|
|
|
367
371
|
applyModule(moduleId: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
|
|
@@ -417,7 +421,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
417
421
|
}
|
|
418
422
|
runStrictValidation(next, issues, `domains.${domainKey}.`);
|
|
419
423
|
issues.push(...registryCheck(next));
|
|
420
|
-
return finalize(next, [], issues, writeOpts.dryRun
|
|
424
|
+
return finalize(next, [], issues, writeOpts.dryRun);
|
|
421
425
|
},
|
|
422
426
|
|
|
423
427
|
applyService(service: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
|
|
@@ -474,7 +478,7 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
474
478
|
const dryRun = writeOpts.dryRun === true;
|
|
475
479
|
const applied = writeCredentialArtifacts(appDir, plan, dryRun);
|
|
476
480
|
issues.push(...registryCheck(next));
|
|
477
|
-
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun
|
|
481
|
+
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun);
|
|
478
482
|
},
|
|
479
483
|
|
|
480
484
|
deleteService(service: string): SaveResult {
|
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
import type { LayerIntegrationSchemas } from '../dto.js';
|
|
1
|
+
import type { IntegrationFieldRegistryView, LayerIntegrationSchemas, ServiceIntegrationSchemas } from '../dto.js';
|
|
2
2
|
import { isSupplied, type ViewSnapshot } from './context.js';
|
|
3
|
-
import {
|
|
4
|
-
import { moduleDomainWritable, providerFootprint, resolveDomainBinding } from '../integrations/domain-binding.js';
|
|
3
|
+
import { resolveServiceConfigSchema } from './providers.js';
|
|
4
|
+
import { domainOfService, moduleDomainWritable, providerFootprint, resolveDomainBinding } from '../integrations/domain-binding.js';
|
|
5
|
+
import { isControlledInlineProvider } from '../integrations/predicate.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
* config
|
|
9
|
-
* /app、/modules/:m include)/ 供给轴批量(providers.ts include
|
|
8
|
+
* 层级/服务级 config schema 计算视图(v4.4 D32——静态轴富化,零 contracts 求值;v4.7 D38 服务级)。
|
|
9
|
+
* config 表单结构四形态同源(P10):槽级组合单体(providers.ts)/ 层级内嵌(本文件——
|
|
10
|
+
* /app、/modules/:m include)/ 供给轴批量(providers.ts include——切换态)/ 服务级生效绑定
|
|
11
|
+
* (本文件 loadServiceIntegrationSchemas——/services/:s include)。
|
|
10
12
|
*
|
|
11
13
|
* 计算规则(D32 裁决):禁交集——config 五层浅合并下「部分服务认识的字段」合法(谓词 #7
|
|
12
14
|
* 并集键集同依据);禁合成单一 schema——required/title 冲突的有损代数;产物 = 扁平映射,
|
|
13
15
|
* 分组归客户端纯呈现。层级 schema = 表单引导**非写入门禁**(P16——写路径校验权威恒在谓词
|
|
14
16
|
* #7 键集 + ajv 凭据链;root 级口径差:#7 = 引用厂商并集,表单 = 层生效厂商单厂商作用域)。
|
|
17
|
+
*
|
|
18
|
+
* v4.6(D36/D37 additive 精化):**原始投影**(loadApp/loadModuleIntegrationSchemas)原样保留
|
|
19
|
+
* 为诊断面 / registry 输入;**消费投影** = buildIntegrationFieldRegistry(字段级合并代数
|
|
20
|
+
* R1/R2/R3 服务端单源,conflict 折叠 schema:null);**轴向假想绑定** = formProviderOf
|
|
21
|
+
* (override ?? 实际解析——双投影同源同口径,formViews 装配层一次计算两投影)。
|
|
22
|
+
* 单源三锚:层级 schema 计算 = resolveLayerIntegrationSchemas / 生效厂商解析 = formProviderOf /
|
|
23
|
+
* 合并代数 = buildIntegrationFieldRegistry。
|
|
15
24
|
*/
|
|
16
25
|
|
|
17
|
-
/** 核心纯函数:显式作用域(provider null → 整字段 null;services 逐项
|
|
18
|
-
*
|
|
26
|
+
/** 核心纯函数:显式作用域(provider null → 整字段 null;services 逐项 resolveServiceConfigSchema
|
|
27
|
+
* 复合解析——v4.7 D38 收口,零新读取路径;供给在场未声明 = null,mock 合法) */
|
|
28
|
+
export function resolveLayerIntegrationSchemas(snapshot: ViewSnapshot, provider: string, services: readonly string[]): LayerIntegrationSchemas;
|
|
29
|
+
export function resolveLayerIntegrationSchemas(snapshot: ViewSnapshot, provider: string | null, services: readonly string[]): LayerIntegrationSchemas | null;
|
|
19
30
|
export function resolveLayerIntegrationSchemas(
|
|
20
31
|
snapshot: ViewSnapshot,
|
|
21
32
|
provider: string | null,
|
|
@@ -24,30 +35,192 @@ export function resolveLayerIntegrationSchemas(
|
|
|
24
35
|
if (provider === null) return null;
|
|
25
36
|
const out: LayerIntegrationSchemas['services'] = {};
|
|
26
37
|
for (const s of services) {
|
|
27
|
-
|
|
28
|
-
out[s] = { configSchema: entry ? readSchemaRef(snapshot.appDir, entry.packageDir, entry.configSchema) : null };
|
|
38
|
+
out[s] = { configSchema: resolveServiceConfigSchema(snapshot, provider, s) };
|
|
29
39
|
}
|
|
30
40
|
return { provider, services: out };
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
43
|
+
// ── v4.6 D36/D37:合并代数助手(内联——深相等判定基)──
|
|
44
|
+
|
|
45
|
+
/** 稳定序列化(键序归一——R2 深相等 / R1 签名比较统一基;数组保序、对象键 sort) */
|
|
46
|
+
function stableSerialize(v: unknown): string {
|
|
47
|
+
if (v === null || typeof v !== 'object') return JSON.stringify(v ?? null) as string;
|
|
48
|
+
if (Array.isArray(v)) return `[${v.map(stableSerialize).join(',')}]`;
|
|
49
|
+
const keys = Object.keys(v as Record<string, unknown>).sort();
|
|
50
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableSerialize((v as Record<string, unknown>)[k])}`).join(',')}}`;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
/** enum 缺席哨兵(≠ 任何实际集签名——混布必不等) */
|
|
54
|
+
const NO_ENUM = '\u0000no-enum';
|
|
55
|
+
|
|
56
|
+
/** enum 集签名(去重排序——序不敏感) */
|
|
57
|
+
function enumSig(sub: Record<string, unknown>): string {
|
|
58
|
+
const e = sub.enum;
|
|
59
|
+
if (!Array.isArray(e)) return NO_ENUM;
|
|
60
|
+
return `[${[...new Set(e.map(stableSerialize))].sort().join(',')}]`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 顶层 type 签名(数组形态序敏感——严格全等);非对象(含布尔子 schema)/ type 缺席 → null(R3) */
|
|
64
|
+
function typeSig(sub: unknown): string | null {
|
|
65
|
+
if (!sub || typeof sub !== 'object' || Array.isArray(sub)) return null;
|
|
66
|
+
const t = (sub as Record<string, unknown>).type;
|
|
67
|
+
return t === undefined ? null : stableSerialize(t);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** D36 合并代数单点(R1/R2/R3——唯一扩展点;保守降级:不可调和 → null,客户端 RawJson 保真) */
|
|
71
|
+
function mergeFieldSources(sources: ReadonlyArray<{ subschema: unknown }>): object | null {
|
|
72
|
+
const sigs = sources.map((s) => typeSig(s.subschema));
|
|
73
|
+
if (sigs.some((s) => s === null) || new Set(sigs).size > 1) return null; // R3:非对象/type 缺席/type 不等
|
|
74
|
+
if (sigs[0] === '"object"') {
|
|
75
|
+
// R2:全源稳定序列化深相等 → 首源;异构 → null
|
|
76
|
+
const first = stableSerialize(sources[0].subschema);
|
|
77
|
+
return sources.every((s) => stableSerialize(s.subschema) === first) ? (sources[0].subschema as object) : null;
|
|
78
|
+
}
|
|
79
|
+
// R1:标量型——enum 集全等(含双无哨兵)→ 首源;不等/混布 → null
|
|
80
|
+
const es = new Set(sources.map((s) => enumSig(s.subschema as Record<string, unknown>)));
|
|
81
|
+
return es.size === 1 ? (sources[0].subschema as object) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** D36 registry 构造(纯函数——消费 resolveLayerIntegrationSchemas 产物,禁旁路直取 serviceSchemas)。
|
|
85
|
+
* fields 收集零过滤(布尔/非对象子 schema 入列,判定全权归 mergeFieldSources——S8 互证等式由
|
|
86
|
+
* 收集定义结构性闭合);schemaless 仅 configSchema === null(布尔/畸形整 schema 防御跳过——
|
|
87
|
+
* 不入 schemaless、零参与聚合;D32 运行时畸形,非契约面)。 */
|
|
88
|
+
export function buildIntegrationFieldRegistry(
|
|
89
|
+
layer: LayerIntegrationSchemas,
|
|
90
|
+
o?: { unsupplied?: string[] },
|
|
91
|
+
): IntegrationFieldRegistryView {
|
|
92
|
+
const schemaless: string[] = [];
|
|
93
|
+
let additionalPropertiesAllowed = true;
|
|
94
|
+
const buckets = new Map<string, Array<{ service: string; subschema: unknown; required: boolean }>>();
|
|
95
|
+
for (const [service, entry] of Object.entries(layer.services)) {
|
|
96
|
+
// 插入序 = scope 遍历序(app 字母序 / module demand 序)——fields 首声明序由此保证
|
|
97
|
+
const schema = entry.configSchema;
|
|
98
|
+
if (schema === null) {
|
|
99
|
+
schemaless.push(service);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) continue;
|
|
103
|
+
const s = schema as Record<string, unknown>;
|
|
104
|
+
if (s.additionalProperties === false) additionalPropertiesAllowed = false;
|
|
105
|
+
const props = s.properties;
|
|
106
|
+
if (!props || typeof props !== 'object' || Array.isArray(props)) continue; // ≡ 谓词 collectPropertyKeys 守卫
|
|
107
|
+
const required = Array.isArray(s.required) ? s.required : [];
|
|
108
|
+
for (const key of Object.keys(props)) {
|
|
109
|
+
const bucket = buckets.get(key) ?? [];
|
|
110
|
+
bucket.push({ service, subschema: (props as Record<string, unknown>)[key], required: required.includes(key) });
|
|
111
|
+
buckets.set(key, bucket);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const fields: IntegrationFieldRegistryView['fields'] = [];
|
|
115
|
+
for (const [key, sources] of buckets) {
|
|
116
|
+
fields.push({
|
|
117
|
+
key,
|
|
118
|
+
schema: mergeFieldSources(sources),
|
|
119
|
+
sources: sources.map(({ service, required }) => ({ service, required })),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
provider: layer.provider,
|
|
124
|
+
schemaless,
|
|
125
|
+
unsupplied: [...(o?.unsupplied ?? [])],
|
|
126
|
+
additionalPropertiesAllowed,
|
|
127
|
+
fields,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** D37 生效厂商解析单源(实际/假想同径):override(非空串)?? 实际解析。
|
|
132
|
+
* 空串归一(I3)单点;module 轴 D34 不可写/不在册 → null(override 不豁免——预览服务于保存)。 */
|
|
133
|
+
export function formProviderOf(
|
|
134
|
+
axis: 'app' | 'module',
|
|
135
|
+
snapshot: ViewSnapshot,
|
|
136
|
+
moduleId?: string,
|
|
137
|
+
override?: string,
|
|
138
|
+
): string | null {
|
|
139
|
+
const ovr = typeof override === 'string' && override.length > 0 ? override : undefined;
|
|
140
|
+
if (axis === 'app') {
|
|
141
|
+
if (ovr !== undefined) return ovr;
|
|
142
|
+
const p = (snapshot.integrations as { provider?: unknown } | null)?.provider;
|
|
143
|
+
return typeof p === 'string' && p.length > 0 ? p : null;
|
|
144
|
+
}
|
|
45
145
|
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
46
146
|
if (!mod) return null; // 视图已先行 404,此处防御
|
|
47
147
|
const w = moduleDomainWritable(snapshot.walk, mod);
|
|
48
|
-
if (!w.writable) return null;
|
|
49
|
-
|
|
50
|
-
|
|
148
|
+
if (!w.writable) return null; // D34 不豁免
|
|
149
|
+
if (ovr !== undefined) return ovr;
|
|
150
|
+
return resolveDomainBinding(snapshot.integrations, w.domainKey).provider;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** D36/D37 表单层双投影(I1:同源同口径一次计算——双取单算 by construction) */
|
|
154
|
+
export interface LayerFormViews {
|
|
155
|
+
integrationSchemas: LayerIntegrationSchemas | null;
|
|
156
|
+
integrationFieldRegistry: IntegrationFieldRegistryView | null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 防御双 null(module 不在册/不可写/未绑定、app 未绑定——F3) */
|
|
160
|
+
const NULL_FORM_VIEWS: LayerFormViews = { integrationSchemas: null, integrationFieldRegistry: null };
|
|
161
|
+
|
|
162
|
+
/** app 轴装配:scope = 供给足迹全量(≠ AppView.services——root.config 流向包括模块服务在内的所有服务);
|
|
163
|
+
* unsupplied 恒 [](结构性:足迹 ⊆ 供给)。未知厂商/零足迹 → 非 null 空集(镜像 D32 mock 语义)。 */
|
|
164
|
+
export function loadAppFormViews(snapshot: ViewSnapshot, appProvider?: string): LayerFormViews {
|
|
165
|
+
const provider = formProviderOf('app', snapshot, undefined, appProvider);
|
|
166
|
+
if (provider === null) return NULL_FORM_VIEWS;
|
|
167
|
+
const layer = resolveLayerIntegrationSchemas(snapshot, provider, providerFootprint(snapshot.catalog, provider));
|
|
168
|
+
return { integrationSchemas: layer, integrationFieldRegistry: buildIntegrationFieldRegistry(layer) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** module 轴装配:仅 writable 单域(D34);scope = demand ∩ 该厂商供给;unsupplied 同点派生
|
|
172
|
+
* (demand − 供给,Set 去重保序——manifest demand 不去重防御)。 */
|
|
173
|
+
export function loadModuleFormViews(snapshot: ViewSnapshot, moduleId: string, moduleProvider?: string): LayerFormViews {
|
|
174
|
+
const provider = formProviderOf('module', snapshot, moduleId, moduleProvider);
|
|
175
|
+
const mod = snapshot.walk.modules.find((m) => m.id === moduleId);
|
|
176
|
+
if (provider === null || !mod) return NULL_FORM_VIEWS;
|
|
51
177
|
const demand = (mod.descriptor.contributes.services ?? []).map((s) => s.service);
|
|
52
|
-
|
|
178
|
+
const supplied = (s: string): boolean => isSupplied(snapshot.catalog, provider, s);
|
|
179
|
+
const layer = resolveLayerIntegrationSchemas(snapshot, provider, demand.filter(supplied));
|
|
180
|
+
return {
|
|
181
|
+
integrationSchemas: layer,
|
|
182
|
+
integrationFieldRegistry: buildIntegrationFieldRegistry(layer, {
|
|
183
|
+
unsupplied: [...new Set(demand.filter((s) => !supplied(s)))],
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** app 级 loader(v4.6 委托化——签名零改动,公开面保持;假想态归 loadAppFormViews override 参) */
|
|
189
|
+
export function loadAppIntegrationSchemas(snapshot: ViewSnapshot): LayerIntegrationSchemas | null {
|
|
190
|
+
return loadAppFormViews(snapshot).integrationSchemas;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** module 级 loader(v4.6 委托化——同上;多域/零域/共享键/未绑定 → 整字段 null,表单降级 = ExtraFields) */
|
|
194
|
+
export function loadModuleIntegrationSchemas(snapshot: ViewSnapshot, moduleId: string): LayerIntegrationSchemas | null {
|
|
195
|
+
return loadModuleFormViews(snapshot, moduleId).integrationSchemas;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 服务级 loader(v4.7 D38):四分支穷举(node.provider = 文件 services[s].provider 逐字原值;
|
|
199
|
+
* 实例层无条件排除——node 级口径);非空三分支归约一次复合解析。
|
|
200
|
+
* ① undefined/'' → resolveDomainBinding 域级联(域 + root 双缺 → 整字段 null)
|
|
201
|
+
* ② 非空 string → bare:catalog miss(未知厂商/未供给)→ configSchema:null 不 404(富化非门控)
|
|
202
|
+
* ③ 受控内联(isControlledInlineProvider 单源)→ 名 = p.provider;catalog 复合键 > p.configSchema ref
|
|
203
|
+
* (H9 逃逸 400 照抛——readInlineSchema 共享路径)
|
|
204
|
+
* ④ 其余(null/数组/原始值/坏对象)→ 整字段 null(≡ 谓词 #5 INLINE_PROVIDER_INVALID 同判——
|
|
205
|
+
* 不放行域级联,防错误 schema 污染坏节点) */
|
|
206
|
+
export function loadServiceIntegrationSchemas(
|
|
207
|
+
snapshot: ViewSnapshot,
|
|
208
|
+
service: string,
|
|
209
|
+
): ServiceIntegrationSchemas | null {
|
|
210
|
+
const p = (snapshot.integrations as { services?: Record<string, { provider?: unknown }> } | null)
|
|
211
|
+
?.services?.[service]?.provider;
|
|
212
|
+
if (p === undefined || p === '') {
|
|
213
|
+
const binding = resolveDomainBinding(snapshot.integrations, domainOfService(service));
|
|
214
|
+
return binding.provider === null
|
|
215
|
+
? null
|
|
216
|
+
: { provider: binding.provider, configSchema: resolveServiceConfigSchema(snapshot, binding.provider, service) };
|
|
217
|
+
}
|
|
218
|
+
if (typeof p === 'string') {
|
|
219
|
+
return { provider: p, configSchema: resolveServiceConfigSchema(snapshot, p, service) };
|
|
220
|
+
}
|
|
221
|
+
if (isControlledInlineProvider(p)) {
|
|
222
|
+
const decl = p as { provider: string; configSchema?: string };
|
|
223
|
+
return { provider: decl.provider, configSchema: resolveServiceConfigSchema(snapshot, decl.provider, service, decl.configSchema) };
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
53
226
|
}
|