@nocobase/flow-engine 2.1.0-alpha.10 → 2.1.0-alpha.11
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/lib/FlowDefinition.d.ts +4 -0
- package/lib/FlowSchemaRegistry.d.ts +154 -0
- package/lib/FlowSchemaRegistry.js +1427 -0
- package/lib/components/subModel/AddSubModelButton.js +11 -0
- package/lib/flow-schema-registry/fieldBinding.d.ts +32 -0
- package/lib/flow-schema-registry/fieldBinding.js +165 -0
- package/lib/flow-schema-registry/modelPatches.d.ts +16 -0
- package/lib/flow-schema-registry/modelPatches.js +235 -0
- package/lib/flow-schema-registry/schemaInference.d.ts +17 -0
- package/lib/flow-schema-registry/schemaInference.js +207 -0
- package/lib/flow-schema-registry/utils.d.ts +25 -0
- package/lib/flow-schema-registry/utils.js +293 -0
- package/lib/flowEngine.js +4 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +3 -1
- package/lib/models/DisplayItemModel.d.ts +1 -1
- package/lib/models/EditableItemModel.d.ts +1 -1
- package/lib/models/FilterableItemModel.d.ts +1 -1
- package/lib/runjs-context/setup.js +1 -0
- package/lib/server.d.ts +10 -0
- package/lib/server.js +32 -0
- package/lib/types.d.ts +233 -0
- package/package.json +4 -4
- package/server.d.ts +1 -0
- package/server.js +1 -0
- package/src/FlowSchemaRegistry.ts +1799 -0
- package/src/__tests__/FlowSchemaRegistry.test.ts +1951 -0
- package/src/__tests__/flow-engine.test.ts +48 -0
- package/src/__tests__/flowEngine.modelLoaders.test.ts +5 -1
- package/src/__tests__/flowEngine.saveModel.test.ts +4 -0
- package/src/__tests__/runjsContext.test.ts +3 -0
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/components/subModel/AddSubModelButton.tsx +15 -1
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +1 -0
- package/src/flow-schema-registry/fieldBinding.ts +171 -0
- package/src/flow-schema-registry/modelPatches.ts +260 -0
- package/src/flow-schema-registry/schemaInference.ts +210 -0
- package/src/flow-schema-registry/utils.ts +268 -0
- package/src/flowEngine.ts +7 -1
- package/src/index.ts +1 -0
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/runjs-context/setup.ts +1 -0
- package/src/server.ts +11 -0
- package/src/types.ts +273 -0
|
@@ -89,7 +89,13 @@ describe('FlowEngine', () => {
|
|
|
89
89
|
class MockFlowModelRepository implements IFlowModelRepository {
|
|
90
90
|
// 使用可配置返回值,便于不同用例控制 findOne 行为
|
|
91
91
|
findOneResult: any = null;
|
|
92
|
+
ensureResult: any = null;
|
|
93
|
+
ensureCalls = 0;
|
|
92
94
|
save = vi.fn(async (model: FlowModel) => ({ success: true, uid: model.uid }));
|
|
95
|
+
async ensure() {
|
|
96
|
+
this.ensureCalls += 1;
|
|
97
|
+
return this.ensureResult ? JSON.parse(JSON.stringify(this.ensureResult)) : null;
|
|
98
|
+
}
|
|
93
99
|
async findOne() {
|
|
94
100
|
// 返回深拷贝,避免被测试过程修改
|
|
95
101
|
return this.findOneResult ? JSON.parse(JSON.stringify(this.findOneResult)) : null;
|
|
@@ -188,5 +194,47 @@ describe('FlowEngine', () => {
|
|
|
188
194
|
expect(Array.isArray(mounted)).toBe(false);
|
|
189
195
|
expect(mounted?.uid).toBe('c3');
|
|
190
196
|
});
|
|
197
|
+
|
|
198
|
+
it('should call repository.ensure with repository context preserved', async () => {
|
|
199
|
+
const parent = engine.createModel({ uid: 'p4', use: 'FlowModel' });
|
|
200
|
+
|
|
201
|
+
repo.ensureResult = {
|
|
202
|
+
uid: 'c4',
|
|
203
|
+
use: 'FlowModel',
|
|
204
|
+
parentId: parent.uid,
|
|
205
|
+
subKey: 'page',
|
|
206
|
+
subType: 'object',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const child = await engine.loadOrCreateModel({
|
|
210
|
+
parentId: parent.uid,
|
|
211
|
+
subKey: 'page',
|
|
212
|
+
subType: 'object',
|
|
213
|
+
use: 'FlowModel',
|
|
214
|
+
async: true,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(child).toBeTruthy();
|
|
218
|
+
expect(repo.ensureCalls).toBe(1);
|
|
219
|
+
expect((parent.subModels as any).page).toBe(child);
|
|
220
|
+
expect(repo.save).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should not persist through ensure when skipSave is true', async () => {
|
|
224
|
+
repo.findOneResult = null;
|
|
225
|
+
|
|
226
|
+
const model = await engine.loadOrCreateModel(
|
|
227
|
+
{
|
|
228
|
+
uid: 'c5',
|
|
229
|
+
use: 'FlowModel',
|
|
230
|
+
},
|
|
231
|
+
{ skipSave: true },
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(model).toBeTruthy();
|
|
235
|
+
expect(model?.uid).toBe('c5');
|
|
236
|
+
expect(repo.ensureCalls).toBe(0);
|
|
237
|
+
expect(repo.save).not.toHaveBeenCalled();
|
|
238
|
+
});
|
|
191
239
|
});
|
|
192
240
|
});
|
|
@@ -16,10 +16,14 @@ class MockFlowModelRepository implements IFlowModelRepository {
|
|
|
16
16
|
findOneResult: any = null;
|
|
17
17
|
save = vi.fn(async (model: FlowModel) => ({ success: true, uid: model.uid }));
|
|
18
18
|
|
|
19
|
-
async findOne() {
|
|
19
|
+
async findOne(_query?: any) {
|
|
20
20
|
return this.findOneResult ? JSON.parse(JSON.stringify(this.findOneResult)) : null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
async ensure(options: any) {
|
|
24
|
+
return await this.findOne(options);
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
async destroy() {
|
|
24
28
|
return true;
|
|
25
29
|
}
|
|
@@ -38,6 +38,7 @@ describe('flowRunJSContext registry and doc', () => {
|
|
|
38
38
|
'JSBlockModel',
|
|
39
39
|
'JSFieldModel',
|
|
40
40
|
'JSItemModel',
|
|
41
|
+
'JSItemActionModel',
|
|
41
42
|
'JSColumnModel',
|
|
42
43
|
'FormJSFieldItemModel',
|
|
43
44
|
'JSRecordActionModel',
|
|
@@ -58,8 +59,10 @@ describe('flowRunJSContext registry and doc', () => {
|
|
|
58
59
|
it('should expose scene metadata for contexts', () => {
|
|
59
60
|
expect(getRunJSScenesForModel('JSBlockModel', 'v1')).toEqual(['block']);
|
|
60
61
|
expect(getRunJSScenesForModel('JSFieldModel', 'v1')).toEqual(['detail']);
|
|
62
|
+
expect(getRunJSScenesForModel('JSItemActionModel', 'v1')).toEqual(['table']);
|
|
61
63
|
expect(getRunJSScenesForModel('JSBlockModel', 'v2')).toEqual(['block']);
|
|
62
64
|
expect(getRunJSScenesForModel('JSFieldModel', 'v2')).toEqual(['detail']);
|
|
65
|
+
expect(getRunJSScenesForModel('JSItemActionModel', 'v2')).toEqual(['table']);
|
|
63
66
|
expect(getRunJSScenesForModel('UnknownModel', 'v1')).toEqual([]);
|
|
64
67
|
expect(getRunJSScenesForModel('UnknownModel', 'v2')).toEqual([]);
|
|
65
68
|
});
|
|
@@ -186,6 +186,7 @@ describe('RunJS Context Runtime Behavior', () => {
|
|
|
186
186
|
'JSBlockModel',
|
|
187
187
|
'JSFieldModel',
|
|
188
188
|
'JSItemModel',
|
|
189
|
+
'JSItemActionModel',
|
|
189
190
|
'JSColumnModel',
|
|
190
191
|
'FormJSFieldItemModel',
|
|
191
192
|
'JSRecordActionModel',
|
|
@@ -237,6 +238,7 @@ describe('RunJS Context Runtime Behavior', () => {
|
|
|
237
238
|
'JSBlockModel',
|
|
238
239
|
'JSFieldModel',
|
|
239
240
|
'JSItemModel',
|
|
241
|
+
'JSItemActionModel',
|
|
240
242
|
'JSColumnModel',
|
|
241
243
|
'FormJSFieldItemModel',
|
|
242
244
|
'JSRecordActionModel',
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { Switch } from 'antd';
|
|
11
11
|
import _ from 'lodash';
|
|
12
|
-
import React, { useMemo } from 'react';
|
|
12
|
+
import React, { useEffect, useMemo } from 'react';
|
|
13
13
|
import { FlowModelContext } from '../../flowContext';
|
|
14
14
|
import { FlowModel } from '../../models';
|
|
15
15
|
import { CreateModelOptions, ModelConstructor } from '../../types';
|
|
@@ -667,6 +667,20 @@ const AddSubModelButtonCore = function AddSubModelButton({
|
|
|
667
667
|
[finalItems, model, subModelKey, subModelType],
|
|
668
668
|
);
|
|
669
669
|
|
|
670
|
+
useEffect(() => {
|
|
671
|
+
const handleSubModelChange = () => {
|
|
672
|
+
setRefreshTick((x) => x + 1);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
model.emitter.on('onSubModelAdded', handleSubModelChange);
|
|
676
|
+
model.emitter.on('onSubModelRemoved', handleSubModelChange);
|
|
677
|
+
|
|
678
|
+
return () => {
|
|
679
|
+
model.emitter.off('onSubModelAdded', handleSubModelChange);
|
|
680
|
+
model.emitter.off('onSubModelRemoved', handleSubModelChange);
|
|
681
|
+
};
|
|
682
|
+
}, [model]);
|
|
683
|
+
|
|
670
684
|
return (
|
|
671
685
|
<LazyDropdown
|
|
672
686
|
menu={{
|
|
@@ -1117,6 +1117,7 @@ describe('AddSubModelButton toggleable behavior', () => {
|
|
|
1117
1117
|
// Minimal fake repository for save/destroy
|
|
1118
1118
|
class FakeRepo implements IFlowModelRepository<any> {
|
|
1119
1119
|
findOne = vi.fn().mockResolvedValue(null);
|
|
1120
|
+
ensure = vi.fn(async (values: any) => await this.findOne(values));
|
|
1120
1121
|
save = vi.fn().mockResolvedValue({});
|
|
1121
1122
|
destroy = vi.fn().mockResolvedValue(true);
|
|
1122
1123
|
move = vi.fn().mockResolvedValue(undefined);
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import _ from 'lodash';
|
|
11
|
+
import type {
|
|
12
|
+
FlowFieldBindingConditions,
|
|
13
|
+
FlowFieldBindingContextContribution,
|
|
14
|
+
FlowFieldBindingContribution,
|
|
15
|
+
FlowFieldModelCompatibility,
|
|
16
|
+
FlowSchemaCoverage,
|
|
17
|
+
} from '../types';
|
|
18
|
+
import { normalizeStringArray } from './utils';
|
|
19
|
+
|
|
20
|
+
export type RegisteredFieldBindingContext = {
|
|
21
|
+
name: string;
|
|
22
|
+
inherits: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type RegisteredFieldBinding = {
|
|
26
|
+
context: string;
|
|
27
|
+
use: string;
|
|
28
|
+
interfaces: string[];
|
|
29
|
+
isDefault: boolean;
|
|
30
|
+
order?: number;
|
|
31
|
+
conditions?: FlowFieldBindingConditions;
|
|
32
|
+
defaultProps?: any;
|
|
33
|
+
source: FlowSchemaCoverage['source'];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function normalizeFieldBindingConditions(
|
|
37
|
+
conditions?: FlowFieldBindingConditions,
|
|
38
|
+
): FlowFieldBindingConditions | undefined {
|
|
39
|
+
if (!conditions || typeof conditions !== 'object' || Array.isArray(conditions)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const normalized = _.pickBy(
|
|
44
|
+
{
|
|
45
|
+
association: typeof conditions.association === 'boolean' ? conditions.association : undefined,
|
|
46
|
+
fieldTypes: normalizeStringArray(conditions.fieldTypes),
|
|
47
|
+
targetCollectionTemplateIn: normalizeStringArray(conditions.targetCollectionTemplateIn),
|
|
48
|
+
targetCollectionTemplateNotIn: normalizeStringArray(conditions.targetCollectionTemplateNotIn),
|
|
49
|
+
},
|
|
50
|
+
(value) => {
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
return value.length > 0;
|
|
53
|
+
}
|
|
54
|
+
return value !== undefined;
|
|
55
|
+
},
|
|
56
|
+
) as FlowFieldBindingConditions;
|
|
57
|
+
|
|
58
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeFieldBindingContextContribution(
|
|
62
|
+
contribution?: FlowFieldBindingContextContribution,
|
|
63
|
+
fallbackName?: string,
|
|
64
|
+
): RegisteredFieldBindingContext | undefined {
|
|
65
|
+
const name = String(contribution?.name || fallbackName || '').trim();
|
|
66
|
+
if (!name) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
name,
|
|
72
|
+
inherits: normalizeStringArray(contribution?.inherits),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function normalizeFieldBindingContribution(
|
|
77
|
+
contribution?: FlowFieldBindingContribution,
|
|
78
|
+
source: FlowSchemaCoverage['source'] = 'official',
|
|
79
|
+
): RegisteredFieldBinding | undefined {
|
|
80
|
+
const context = String(contribution?.context || '').trim();
|
|
81
|
+
const use = String(contribution?.use || '').trim();
|
|
82
|
+
const interfaces = normalizeStringArray(contribution?.interfaces);
|
|
83
|
+
if (!context || !use || interfaces.length === 0) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
context,
|
|
89
|
+
use,
|
|
90
|
+
interfaces,
|
|
91
|
+
isDefault: contribution?.isDefault === true,
|
|
92
|
+
order: typeof contribution?.order === 'number' ? contribution.order : undefined,
|
|
93
|
+
conditions: normalizeFieldBindingConditions(contribution?.conditions),
|
|
94
|
+
defaultProps: contribution?.defaultProps === undefined ? undefined : _.cloneDeep(contribution.defaultProps),
|
|
95
|
+
source,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function matchesFieldBinding(
|
|
100
|
+
binding: RegisteredFieldBinding,
|
|
101
|
+
options: {
|
|
102
|
+
interface?: string;
|
|
103
|
+
fieldType?: string;
|
|
104
|
+
association?: boolean;
|
|
105
|
+
targetCollectionTemplate?: string;
|
|
106
|
+
},
|
|
107
|
+
) {
|
|
108
|
+
if (options.interface && !binding.interfaces.includes('*') && !binding.interfaces.includes(options.interface)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const conditions = binding.conditions;
|
|
113
|
+
if (!conditions) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (typeof conditions.association === 'boolean' && options.association !== undefined) {
|
|
118
|
+
if (conditions.association !== options.association) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (conditions.fieldTypes?.length && options.fieldType) {
|
|
124
|
+
if (!conditions.fieldTypes.includes(options.fieldType)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (conditions.targetCollectionTemplateIn?.length && options.targetCollectionTemplate) {
|
|
130
|
+
if (!conditions.targetCollectionTemplateIn.includes(options.targetCollectionTemplate)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (conditions.targetCollectionTemplateNotIn?.length && options.targetCollectionTemplate) {
|
|
136
|
+
if (conditions.targetCollectionTemplateNotIn.includes(options.targetCollectionTemplate)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function buildFieldModelCompatibility(binding: RegisteredFieldBinding): FlowFieldModelCompatibility {
|
|
145
|
+
const compatibility: FlowFieldModelCompatibility = {
|
|
146
|
+
context: binding.context,
|
|
147
|
+
interfaces: _.cloneDeep(binding.interfaces),
|
|
148
|
+
inheritParentFieldBinding: true,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (binding.isDefault) {
|
|
152
|
+
compatibility.isDefault = true;
|
|
153
|
+
}
|
|
154
|
+
if (typeof binding.order === 'number') {
|
|
155
|
+
compatibility.order = binding.order;
|
|
156
|
+
}
|
|
157
|
+
if (typeof binding.conditions?.association === 'boolean') {
|
|
158
|
+
compatibility.association = binding.conditions.association;
|
|
159
|
+
}
|
|
160
|
+
if (binding.conditions?.fieldTypes?.length) {
|
|
161
|
+
compatibility.fieldTypes = _.cloneDeep(binding.conditions.fieldTypes);
|
|
162
|
+
}
|
|
163
|
+
if (binding.conditions?.targetCollectionTemplateIn?.length) {
|
|
164
|
+
compatibility.targetCollectionTemplateIn = _.cloneDeep(binding.conditions.targetCollectionTemplateIn);
|
|
165
|
+
}
|
|
166
|
+
if (binding.conditions?.targetCollectionTemplateNotIn?.length) {
|
|
167
|
+
compatibility.targetCollectionTemplateNotIn = _.cloneDeep(binding.conditions.targetCollectionTemplateNotIn);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return compatibility;
|
|
171
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import _ from 'lodash';
|
|
11
|
+
import type {
|
|
12
|
+
FlowDescendantSchemaPatch,
|
|
13
|
+
FlowJsonSchema,
|
|
14
|
+
FlowModelSchemaPatch,
|
|
15
|
+
FlowSchemaContextEdge,
|
|
16
|
+
FlowSchemaCoverage,
|
|
17
|
+
FlowSubModelContextPathStep,
|
|
18
|
+
FlowSubModelSlotSchema,
|
|
19
|
+
} from '../types';
|
|
20
|
+
import type { RegisteredModelSchema } from '../FlowSchemaRegistry';
|
|
21
|
+
import { deepMergeReplaceArrays, mergeSchemas, normalizeSchemaDocs, normalizeSchemaHints } from './utils';
|
|
22
|
+
|
|
23
|
+
export function normalizeSubModelContextPath(path?: FlowSubModelContextPathStep[]): FlowSubModelContextPathStep[] {
|
|
24
|
+
if (!Array.isArray(path)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return path
|
|
29
|
+
.map((step) => ({
|
|
30
|
+
slotKey: String(step?.slotKey || '').trim(),
|
|
31
|
+
...(typeof step?.use === 'string'
|
|
32
|
+
? { use: step.use.trim() }
|
|
33
|
+
: Array.isArray(step?.use)
|
|
34
|
+
? { use: step.use.map((item) => String(item || '').trim()).filter(Boolean) }
|
|
35
|
+
: {}),
|
|
36
|
+
}))
|
|
37
|
+
.filter((step) => !!step.slotKey);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeModelSchemaPatch(patch?: FlowModelSchemaPatch): FlowModelSchemaPatch | undefined {
|
|
41
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const normalized = _.pickBy(
|
|
46
|
+
{
|
|
47
|
+
stepParamsSchema: patch.stepParamsSchema ? _.cloneDeep(patch.stepParamsSchema) : undefined,
|
|
48
|
+
flowRegistrySchema: patch.flowRegistrySchema ? _.cloneDeep(patch.flowRegistrySchema) : undefined,
|
|
49
|
+
flowRegistrySchemaPatch: patch.flowRegistrySchemaPatch ? _.cloneDeep(patch.flowRegistrySchemaPatch) : undefined,
|
|
50
|
+
subModelSlots: normalizeSubModelSlots(patch.subModelSlots),
|
|
51
|
+
docs: patch.docs ? normalizeSchemaDocs(patch.docs) : undefined,
|
|
52
|
+
examples: Array.isArray(patch.examples) ? _.cloneDeep(patch.examples) : undefined,
|
|
53
|
+
skeleton: patch.skeleton === undefined ? undefined : _.cloneDeep(patch.skeleton),
|
|
54
|
+
dynamicHints: Array.isArray(patch.dynamicHints) ? normalizeSchemaHints(patch.dynamicHints) : undefined,
|
|
55
|
+
},
|
|
56
|
+
(value) => value !== undefined && (!Array.isArray(value) || value.length > 0),
|
|
57
|
+
) as FlowModelSchemaPatch;
|
|
58
|
+
|
|
59
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeDescendantSchemaPatches(
|
|
63
|
+
patches?: FlowDescendantSchemaPatch[],
|
|
64
|
+
): FlowDescendantSchemaPatch[] | undefined {
|
|
65
|
+
if (!Array.isArray(patches)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const normalized = patches
|
|
70
|
+
.map((item) => {
|
|
71
|
+
const path = normalizeSubModelContextPath(item?.path);
|
|
72
|
+
const patch = normalizeModelSchemaPatch(item?.patch);
|
|
73
|
+
if (!patch) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return { path, patch };
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean) as FlowDescendantSchemaPatch[];
|
|
79
|
+
|
|
80
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeChildSchemaPatch(
|
|
84
|
+
patch?: FlowSubModelSlotSchema['childSchemaPatch'],
|
|
85
|
+
): FlowSubModelSlotSchema['childSchemaPatch'] | undefined {
|
|
86
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const directPatch = normalizeModelSchemaPatch(patch as FlowModelSchemaPatch);
|
|
91
|
+
if (directPatch) {
|
|
92
|
+
return directPatch;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const entries = Object.entries(patch as Record<string, FlowModelSchemaPatch>)
|
|
96
|
+
.map(([childUse, childPatch]) => [String(childUse || '').trim(), normalizeModelSchemaPatch(childPatch)] as const)
|
|
97
|
+
.filter(([childUse, childPatch]) => !!childUse && !!childPatch);
|
|
98
|
+
|
|
99
|
+
if (!entries.length) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Object.fromEntries(entries);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function normalizeSubModelSlots(
|
|
107
|
+
slots?: Record<string, FlowSubModelSlotSchema>,
|
|
108
|
+
): Record<string, FlowSubModelSlotSchema> | undefined {
|
|
109
|
+
if (!slots || typeof slots !== 'object' || Array.isArray(slots)) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const normalizedEntries = Object.entries(slots)
|
|
114
|
+
.map(([slotKey, slot]) => {
|
|
115
|
+
if (!slot?.type) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const normalizedSlot: FlowSubModelSlotSchema = {
|
|
120
|
+
type: slot.type,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const normalizedUse = typeof slot.use === 'string' ? slot.use.trim() : undefined;
|
|
124
|
+
if (normalizedUse) {
|
|
125
|
+
normalizedSlot.use = normalizedUse;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const normalizedUses = Array.isArray(slot.uses)
|
|
129
|
+
? slot.uses.map((item) => String(item || '').trim()).filter(Boolean)
|
|
130
|
+
: undefined;
|
|
131
|
+
if (normalizedUses?.length) {
|
|
132
|
+
normalizedSlot.uses = normalizedUses;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (slot.required !== undefined) {
|
|
136
|
+
normalizedSlot.required = slot.required;
|
|
137
|
+
}
|
|
138
|
+
if (slot.type === 'array' && typeof slot.minItems === 'number' && Number.isFinite(slot.minItems)) {
|
|
139
|
+
normalizedSlot.minItems = Math.max(0, Math.trunc(slot.minItems));
|
|
140
|
+
}
|
|
141
|
+
if (slot.dynamic !== undefined) {
|
|
142
|
+
normalizedSlot.dynamic = slot.dynamic;
|
|
143
|
+
}
|
|
144
|
+
if (slot.schema) {
|
|
145
|
+
normalizedSlot.schema = _.cloneDeep(slot.schema);
|
|
146
|
+
}
|
|
147
|
+
if (typeof slot.fieldBindingContext === 'string' && slot.fieldBindingContext.trim()) {
|
|
148
|
+
normalizedSlot.fieldBindingContext = slot.fieldBindingContext.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const childSchemaPatch = normalizeChildSchemaPatch(slot.childSchemaPatch);
|
|
152
|
+
if (childSchemaPatch) {
|
|
153
|
+
normalizedSlot.childSchemaPatch = childSchemaPatch;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const descendantSchemaPatches = normalizeDescendantSchemaPatches(slot.descendantSchemaPatches);
|
|
157
|
+
if (descendantSchemaPatches?.length) {
|
|
158
|
+
normalizedSlot.descendantSchemaPatches = descendantSchemaPatches;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (slot.description !== undefined) {
|
|
162
|
+
normalizedSlot.description = slot.description;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return [slotKey, normalizedSlot] as const;
|
|
166
|
+
})
|
|
167
|
+
.filter(Boolean) as Array<readonly [string, FlowSubModelSlotSchema]>;
|
|
168
|
+
|
|
169
|
+
return normalizedEntries.length > 0 ? Object.fromEntries(normalizedEntries) : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function matchesDescendantSchemaPatch(
|
|
173
|
+
patch: FlowDescendantSchemaPatch,
|
|
174
|
+
remainingEdges: FlowSchemaContextEdge[],
|
|
175
|
+
): boolean {
|
|
176
|
+
const path = normalizeSubModelContextPath(patch.path);
|
|
177
|
+
if (path.length !== remainingEdges.length) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return path.every((step, index) => {
|
|
182
|
+
const edge = remainingEdges[index];
|
|
183
|
+
if (step.slotKey !== edge.slotKey) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (typeof step.use === 'undefined') {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
if (typeof step.use === 'string') {
|
|
190
|
+
return step.use === edge.childUse;
|
|
191
|
+
}
|
|
192
|
+
return step.use.includes(edge.childUse);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function resolveChildSchemaPatch(
|
|
197
|
+
slot: FlowSubModelSlotSchema,
|
|
198
|
+
childUse: string,
|
|
199
|
+
): FlowModelSchemaPatch | undefined {
|
|
200
|
+
const childSchemaPatch = slot.childSchemaPatch;
|
|
201
|
+
if (!childSchemaPatch || typeof childSchemaPatch !== 'object' || Array.isArray(childSchemaPatch)) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const directPatch = normalizeModelSchemaPatch(childSchemaPatch as FlowModelSchemaPatch);
|
|
206
|
+
if (directPatch) {
|
|
207
|
+
return directPatch;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return normalizeModelSchemaPatch((childSchemaPatch as Record<string, FlowModelSchemaPatch>)[childUse]);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function applyModelSchemaPatch(
|
|
214
|
+
target: RegisteredModelSchema,
|
|
215
|
+
patch: FlowModelSchemaPatch,
|
|
216
|
+
source: FlowSchemaCoverage['source'],
|
|
217
|
+
strict?: boolean,
|
|
218
|
+
) {
|
|
219
|
+
target.stepParamsSchema = mergeSchemas(target.stepParamsSchema, patch.stepParamsSchema);
|
|
220
|
+
target.flowRegistrySchema = mergeSchemas(target.flowRegistrySchema, patch.flowRegistrySchema);
|
|
221
|
+
target.flowRegistrySchemaPatch = mergeSchemas(target.flowRegistrySchemaPatch, patch.flowRegistrySchemaPatch);
|
|
222
|
+
target.subModelSlots = normalizeSubModelSlots(
|
|
223
|
+
patch.subModelSlots
|
|
224
|
+
? deepMergeReplaceArrays(target.subModelSlots || {}, patch.subModelSlots)
|
|
225
|
+
: target.subModelSlots,
|
|
226
|
+
);
|
|
227
|
+
target.docs = normalizeSchemaDocs({
|
|
228
|
+
...target.docs,
|
|
229
|
+
...patch.docs,
|
|
230
|
+
examples: patch.docs?.examples || target.docs?.examples,
|
|
231
|
+
dynamicHints: [...(target.docs?.dynamicHints || []), ...(patch.docs?.dynamicHints || [])],
|
|
232
|
+
commonPatterns: patch.docs?.commonPatterns || target.docs?.commonPatterns,
|
|
233
|
+
antiPatterns: patch.docs?.antiPatterns || target.docs?.antiPatterns,
|
|
234
|
+
minimalExample: patch.docs?.minimalExample !== undefined ? patch.docs.minimalExample : target.docs?.minimalExample,
|
|
235
|
+
});
|
|
236
|
+
target.examples = Array.isArray(patch.examples) ? _.cloneDeep(patch.examples) : target.examples;
|
|
237
|
+
target.skeleton =
|
|
238
|
+
patch.skeleton !== undefined ? deepMergeReplaceArrays(target.skeleton, patch.skeleton) : target.skeleton;
|
|
239
|
+
target.dynamicHints = normalizeSchemaHints([
|
|
240
|
+
...(target.dynamicHints || []),
|
|
241
|
+
...(patch.dynamicHints || []),
|
|
242
|
+
...(patch.docs?.dynamicHints || []),
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
const hasSchemaPatch =
|
|
246
|
+
!!patch.stepParamsSchema || !!patch.flowRegistrySchema || !!patch.flowRegistrySchemaPatch || !!patch.subModelSlots;
|
|
247
|
+
if (hasSchemaPatch) {
|
|
248
|
+
target.coverage = {
|
|
249
|
+
...target.coverage,
|
|
250
|
+
status:
|
|
251
|
+
target.coverage.status === 'unresolved'
|
|
252
|
+
? 'manual'
|
|
253
|
+
: target.coverage.status === 'auto'
|
|
254
|
+
? 'mixed'
|
|
255
|
+
: target.coverage.status,
|
|
256
|
+
source: target.coverage.source === 'third-party' ? source : target.coverage.source,
|
|
257
|
+
strict: target.coverage.strict ?? strict,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|