@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.
Files changed (46) hide show
  1. package/lib/FlowDefinition.d.ts +4 -0
  2. package/lib/FlowSchemaRegistry.d.ts +154 -0
  3. package/lib/FlowSchemaRegistry.js +1427 -0
  4. package/lib/components/subModel/AddSubModelButton.js +11 -0
  5. package/lib/flow-schema-registry/fieldBinding.d.ts +32 -0
  6. package/lib/flow-schema-registry/fieldBinding.js +165 -0
  7. package/lib/flow-schema-registry/modelPatches.d.ts +16 -0
  8. package/lib/flow-schema-registry/modelPatches.js +235 -0
  9. package/lib/flow-schema-registry/schemaInference.d.ts +17 -0
  10. package/lib/flow-schema-registry/schemaInference.js +207 -0
  11. package/lib/flow-schema-registry/utils.d.ts +25 -0
  12. package/lib/flow-schema-registry/utils.js +293 -0
  13. package/lib/flowEngine.js +4 -1
  14. package/lib/index.d.ts +1 -0
  15. package/lib/index.js +3 -1
  16. package/lib/models/DisplayItemModel.d.ts +1 -1
  17. package/lib/models/EditableItemModel.d.ts +1 -1
  18. package/lib/models/FilterableItemModel.d.ts +1 -1
  19. package/lib/runjs-context/setup.js +1 -0
  20. package/lib/server.d.ts +10 -0
  21. package/lib/server.js +32 -0
  22. package/lib/types.d.ts +233 -0
  23. package/package.json +4 -4
  24. package/server.d.ts +1 -0
  25. package/server.js +1 -0
  26. package/src/FlowSchemaRegistry.ts +1799 -0
  27. package/src/__tests__/FlowSchemaRegistry.test.ts +1951 -0
  28. package/src/__tests__/flow-engine.test.ts +48 -0
  29. package/src/__tests__/flowEngine.modelLoaders.test.ts +5 -1
  30. package/src/__tests__/flowEngine.saveModel.test.ts +4 -0
  31. package/src/__tests__/runjsContext.test.ts +3 -0
  32. package/src/__tests__/runjsContextRuntime.test.ts +2 -0
  33. package/src/components/subModel/AddSubModelButton.tsx +15 -1
  34. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +1 -0
  35. package/src/flow-schema-registry/fieldBinding.ts +171 -0
  36. package/src/flow-schema-registry/modelPatches.ts +260 -0
  37. package/src/flow-schema-registry/schemaInference.ts +210 -0
  38. package/src/flow-schema-registry/utils.ts +268 -0
  39. package/src/flowEngine.ts +7 -1
  40. package/src/index.ts +1 -0
  41. package/src/models/DisplayItemModel.tsx +1 -1
  42. package/src/models/EditableItemModel.tsx +1 -1
  43. package/src/models/FilterableItemModel.tsx +1 -1
  44. package/src/runjs-context/setup.ts +1 -0
  45. package/src/server.ts +11 -0
  46. 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
  }
@@ -40,6 +40,10 @@ class MockFlowModelRepository implements IFlowModelRepository {
40
40
  return null;
41
41
  }
42
42
 
43
+ async ensure(options: any): Promise<any> {
44
+ return await this.findOne(options);
45
+ }
46
+
43
47
  async destroy(uid: string): Promise<boolean> {
44
48
  return true;
45
49
  }
@@ -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
+ }