@nocobase/flow-engine 2.1.0-alpha.12 → 2.1.0-alpha.13

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 (40) hide show
  1. package/lib/FlowDefinition.d.ts +0 -4
  2. package/lib/index.d.ts +0 -1
  3. package/lib/index.js +1 -3
  4. package/lib/models/DisplayItemModel.d.ts +1 -1
  5. package/lib/models/EditableItemModel.d.ts +1 -1
  6. package/lib/models/FilterableItemModel.d.ts +1 -1
  7. package/lib/runjs-context/snippets/index.js +13 -2
  8. package/lib/{server.d.ts → runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts} +3 -2
  9. package/lib/{server.js → runjs-context/snippets/scene/detail/set-field-style.snippet.js} +27 -9
  10. package/{src/server.ts → lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts} +3 -3
  11. package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
  12. package/lib/types.d.ts +0 -221
  13. package/package.json +4 -4
  14. package/src/__tests__/runjsSnippets.test.ts +21 -0
  15. package/src/index.ts +0 -1
  16. package/src/models/DisplayItemModel.tsx +1 -1
  17. package/src/models/EditableItemModel.tsx +1 -1
  18. package/src/models/FilterableItemModel.tsx +1 -1
  19. package/src/runjs-context/snippets/index.ts +12 -1
  20. package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
  21. package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
  22. package/src/types.ts +0 -262
  23. package/lib/FlowSchemaRegistry.d.ts +0 -154
  24. package/lib/FlowSchemaRegistry.js +0 -1427
  25. package/lib/flow-schema-registry/fieldBinding.d.ts +0 -32
  26. package/lib/flow-schema-registry/fieldBinding.js +0 -165
  27. package/lib/flow-schema-registry/modelPatches.d.ts +0 -16
  28. package/lib/flow-schema-registry/modelPatches.js +0 -235
  29. package/lib/flow-schema-registry/schemaInference.d.ts +0 -17
  30. package/lib/flow-schema-registry/schemaInference.js +0 -207
  31. package/lib/flow-schema-registry/utils.d.ts +0 -25
  32. package/lib/flow-schema-registry/utils.js +0 -293
  33. package/server.d.ts +0 -1
  34. package/server.js +0 -1
  35. package/src/FlowSchemaRegistry.ts +0 -1799
  36. package/src/__tests__/FlowSchemaRegistry.test.ts +0 -1951
  37. package/src/flow-schema-registry/fieldBinding.ts +0 -171
  38. package/src/flow-schema-registry/modelPatches.ts +0 -260
  39. package/src/flow-schema-registry/schemaInference.ts +0 -210
  40. package/src/flow-schema-registry/utils.ts +0 -268
@@ -1,1951 +0,0 @@
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 { describe, expect, it } from 'vitest';
11
- import { FlowSchemaRegistry } from '../FlowSchemaRegistry';
12
- import { FlowModel } from '../models';
13
-
14
- const expectGridLayoutSchemaDocument = (document: any) => {
15
- expect(document?.jsonSchema?.properties?.stepParams).toMatchObject({
16
- properties: {
17
- gridSettings: {
18
- type: 'object',
19
- properties: {
20
- grid: {
21
- type: 'object',
22
- additionalProperties: false,
23
- properties: {
24
- rows: {
25
- type: 'object',
26
- additionalProperties: {
27
- type: 'array',
28
- items: {
29
- type: 'array',
30
- items: {
31
- type: 'string',
32
- },
33
- },
34
- },
35
- },
36
- sizes: {
37
- type: 'object',
38
- additionalProperties: {
39
- type: 'array',
40
- items: {
41
- type: 'number',
42
- },
43
- },
44
- },
45
- rowOrder: {
46
- type: 'array',
47
- items: {
48
- type: 'string',
49
- },
50
- },
51
- },
52
- },
53
- },
54
- },
55
- },
56
- });
57
- };
58
-
59
- const objectSchema = (
60
- properties: Record<string, any> = {},
61
- options: {
62
- required?: string[];
63
- additionalProperties?: boolean | Record<string, any>;
64
- } = {},
65
- ) => {
66
- const { required = [], additionalProperties = true } = options;
67
-
68
- return {
69
- type: 'object',
70
- properties,
71
- ...(required.length ? { required } : {}),
72
- additionalProperties,
73
- };
74
- };
75
-
76
- const modelContribution = (use: string, extra: Record<string, any> = {}) => ({
77
- use,
78
- stepParamsSchema: objectSchema(),
79
- ...extra,
80
- });
81
-
82
- const internalModelContribution = (use: string, uid: string, extra: Record<string, any> = {}) => ({
83
- use,
84
- exposure: 'internal',
85
- stepParamsSchema: objectSchema(),
86
- skeleton: {
87
- uid,
88
- use,
89
- },
90
- ...extra,
91
- });
92
-
93
- const objectSlot = (extra: Record<string, any> = {}) => ({
94
- type: 'object',
95
- ...extra,
96
- });
97
-
98
- const arraySlot = (extra: Record<string, any> = {}) => ({
99
- type: 'array',
100
- ...extra,
101
- });
102
-
103
- describe('FlowSchemaRegistry', () => {
104
- it('should infer JSON schema from static uiSchema', () => {
105
- const registry = new FlowSchemaRegistry();
106
-
107
- registry.registerAction({
108
- name: 'schemaRegistryStaticAction',
109
- handler: () => null,
110
- uiSchema: {
111
- title: {
112
- type: 'string',
113
- required: true,
114
- 'x-validator': {
115
- minLength: 2,
116
- },
117
- } as any,
118
- mode: {
119
- enum: [
120
- { label: 'A', value: 'a' },
121
- { label: 'B', value: 'b' },
122
- ],
123
- } as any,
124
- },
125
- });
126
-
127
- expect(registry.getAction('schemaRegistryStaticAction')?.schema).toMatchObject({
128
- type: 'object',
129
- properties: {
130
- title: {
131
- type: 'string',
132
- minLength: 2,
133
- },
134
- mode: {
135
- enum: ['a', 'b'],
136
- },
137
- },
138
- required: ['title'],
139
- additionalProperties: false,
140
- });
141
- expect(registry.getAction('schemaRegistryStaticAction')?.coverage.status).toBe('auto');
142
- });
143
-
144
- it('should apply action patch and step override with correct priority', () => {
145
- const registry = new FlowSchemaRegistry();
146
-
147
- registry.registerAction({
148
- name: 'schemaRegistryPatchedAction',
149
- handler: () => null,
150
- uiSchema: {
151
- enabled: {
152
- type: 'boolean',
153
- } as any,
154
- },
155
- paramsSchemaPatch: {
156
- properties: {
157
- label: {
158
- type: 'string',
159
- },
160
- },
161
- required: ['label'],
162
- },
163
- });
164
-
165
- const patched = registry.resolveStepParamsSchema(
166
- {
167
- use: 'schemaRegistryPatchedAction',
168
- },
169
- 'SchemaRegistryPatchedModel.settings.save',
170
- );
171
-
172
- expect(patched.schema).toMatchObject({
173
- type: 'object',
174
- properties: {
175
- enabled: {
176
- type: 'boolean',
177
- },
178
- label: {
179
- type: 'string',
180
- },
181
- },
182
- required: ['label'],
183
- additionalProperties: false,
184
- });
185
- expect(patched.coverage).toBe('mixed');
186
-
187
- const overridden = registry.resolveStepParamsSchema(
188
- {
189
- use: 'schemaRegistryPatchedAction',
190
- paramsSchemaOverride: {
191
- type: 'object',
192
- properties: {
193
- only: {
194
- type: 'string',
195
- },
196
- },
197
- required: ['only'],
198
- additionalProperties: false,
199
- },
200
- },
201
- 'SchemaRegistryPatchedModel.settings.save',
202
- );
203
-
204
- expect(overridden.schema).toEqual({
205
- type: 'object',
206
- properties: {
207
- only: {
208
- type: 'string',
209
- },
210
- },
211
- required: ['only'],
212
- additionalProperties: false,
213
- });
214
- expect(overridden.coverage).toBe('manual');
215
- });
216
-
217
- it('should invalidate cached model documents after late action registration', () => {
218
- class SchemaRegistryLateActionHostModel extends FlowModel {}
219
-
220
- SchemaRegistryLateActionHostModel.define({
221
- label: 'Late action host',
222
- });
223
- SchemaRegistryLateActionHostModel.registerFlow({
224
- key: 'settings',
225
- steps: {
226
- save: {
227
- use: 'schemaRegistryLateAction',
228
- },
229
- },
230
- });
231
-
232
- const registry = new FlowSchemaRegistry();
233
- registry.registerModels({
234
- SchemaRegistryLateActionHostModel,
235
- });
236
-
237
- const before = registry.getModelDocument('SchemaRegistryLateActionHostModel');
238
- expect((before.jsonSchema.properties?.stepParams as any)?.properties?.settings?.properties?.save).toMatchObject({
239
- type: 'object',
240
- additionalProperties: true,
241
- });
242
-
243
- registry.registerActionContribution({
244
- name: 'schemaRegistryLateAction',
245
- paramsSchema: {
246
- type: 'object',
247
- properties: {
248
- enabled: {
249
- type: 'boolean',
250
- },
251
- },
252
- required: ['enabled'],
253
- additionalProperties: false,
254
- },
255
- });
256
-
257
- const after = registry.getModelDocument('SchemaRegistryLateActionHostModel');
258
- expect((after.jsonSchema.properties?.stepParams as any)?.properties?.settings?.properties?.save).toMatchObject({
259
- type: 'object',
260
- properties: {
261
- enabled: {
262
- type: 'boolean',
263
- },
264
- },
265
- required: ['enabled'],
266
- additionalProperties: false,
267
- });
268
- expect(after.hash).not.toBe(before.hash);
269
- });
270
-
271
- it('should keep existing coverage metadata when later model contributions only add docs', () => {
272
- const registry = new FlowSchemaRegistry();
273
-
274
- registry.registerModelContribution({
275
- use: 'SchemaRegistryCoverageModel',
276
- stepParamsSchema: objectSchema(),
277
- source: 'official',
278
- strict: true,
279
- });
280
-
281
- registry.registerModelContribution({
282
- use: 'SchemaRegistryCoverageModel',
283
- docs: {
284
- description: 'docs only update',
285
- },
286
- source: 'plugin',
287
- strict: false,
288
- });
289
-
290
- expect(registry.getModel('SchemaRegistryCoverageModel')?.coverage).toEqual({
291
- status: 'manual',
292
- source: 'official',
293
- strict: true,
294
- });
295
- });
296
-
297
- it('should treat flowRegistrySchemaPatch-only contributions as schema coverage', () => {
298
- const registry = new FlowSchemaRegistry();
299
-
300
- registry.registerModelContribution({
301
- use: 'SchemaRegistryFlowRegistryPatchModel',
302
- flowRegistrySchemaPatch: {
303
- properties: {
304
- eventFlow: {
305
- type: 'object',
306
- properties: {
307
- enabled: {
308
- type: 'boolean',
309
- },
310
- },
311
- additionalProperties: false,
312
- },
313
- },
314
- },
315
- source: 'plugin',
316
- strict: true,
317
- });
318
-
319
- expect(registry.getModel('SchemaRegistryFlowRegistryPatchModel')?.coverage).toEqual({
320
- status: 'manual',
321
- source: 'plugin',
322
- strict: true,
323
- });
324
- expect(registry.buildStaticFlowRegistrySchema('SchemaRegistryFlowRegistryPatchModel')).toMatchObject({
325
- properties: {
326
- eventFlow: {
327
- type: 'object',
328
- properties: {
329
- enabled: {
330
- type: 'boolean',
331
- },
332
- },
333
- additionalProperties: false,
334
- },
335
- },
336
- });
337
- });
338
-
339
- it('should resolve slot use expansions from inventory without model-specific hardcoding', () => {
340
- const registry = new FlowSchemaRegistry();
341
-
342
- registry.registerModelContribution(
343
- modelContribution('SchemaRegistryDeclaredSlotChildModel', {
344
- title: 'Declared child',
345
- }),
346
- );
347
- registry.registerModelContribution(
348
- modelContribution('SchemaRegistryExpandedSlotChildModel', {
349
- title: 'Expanded child',
350
- }),
351
- );
352
- registry.registerModelContribution(
353
- modelContribution('SchemaRegistrySlotExpansionParentModel', {
354
- subModelSlots: {
355
- items: arraySlot({
356
- uses: ['SchemaRegistryDeclaredSlotChildModel'],
357
- }),
358
- },
359
- }),
360
- );
361
- registry.registerInventory(
362
- {
363
- slotUseExpansions: [
364
- {
365
- parentUse: 'SchemaRegistrySlotExpansionParentModel',
366
- slotKey: 'items',
367
- uses: ['SchemaRegistryExpandedSlotChildModel', 'SchemaRegistryDeclaredSlotChildModel'],
368
- },
369
- ],
370
- },
371
- 'official',
372
- );
373
-
374
- expect(
375
- registry.resolveSlotAllowedUses(
376
- 'SchemaRegistrySlotExpansionParentModel',
377
- 'items',
378
- registry.getModel('SchemaRegistrySlotExpansionParentModel')?.subModelSlots?.items,
379
- ),
380
- ).toEqual(['SchemaRegistryDeclaredSlotChildModel', 'SchemaRegistryExpandedSlotChildModel']);
381
- expect(registry.getSchemaBundle(['SchemaRegistrySlotExpansionParentModel']).items[0]).toMatchObject({
382
- use: 'SchemaRegistrySlotExpansionParentModel',
383
- subModelCatalog: {
384
- items: {
385
- type: 'array',
386
- candidates: [
387
- expect.objectContaining({ use: 'SchemaRegistryDeclaredSlotChildModel' }),
388
- expect.objectContaining({ use: 'SchemaRegistryExpandedSlotChildModel' }),
389
- ],
390
- },
391
- },
392
- });
393
- });
394
-
395
- it('should accept public tree roots in inventory contributions', () => {
396
- const registry = new FlowSchemaRegistry();
397
-
398
- registry.registerModelContribution(
399
- modelContribution('SchemaRegistryInventoryRootModel', {
400
- exposure: 'internal',
401
- }),
402
- );
403
-
404
- expect(() =>
405
- registry.registerInventory(
406
- {
407
- publicTreeRoots: ['SchemaRegistryInventoryRootModel'],
408
- },
409
- 'official',
410
- ),
411
- ).not.toThrow();
412
- expect(registry.hasQueryableModel('SchemaRegistryInventoryRootModel')).toBe(true);
413
- expect(registry.listPublicTreeRoots()).toEqual(['SchemaRegistryInventoryRootModel']);
414
- });
415
-
416
- it('should build document schema, aggregate flow hints, and infer sub-model slots', () => {
417
- class SchemaRegistryChildModel extends FlowModel {}
418
- class SchemaRegistryParentModel extends FlowModel {}
419
-
420
- SchemaRegistryChildModel.define({
421
- label: 'Schema child',
422
- });
423
- SchemaRegistryParentModel.define({
424
- label: 'Schema parent',
425
- createModelOptions: {
426
- use: 'SchemaRegistryParentModel',
427
- subModels: {
428
- header: {
429
- use: 'SchemaRegistryChildModel',
430
- },
431
- items: [
432
- {
433
- use: 'SchemaRegistryChildModel',
434
- },
435
- ],
436
- },
437
- },
438
- });
439
- SchemaRegistryParentModel.registerFlow({
440
- key: 'settings',
441
- steps: {
442
- rename: {
443
- use: 'schemaRegistryRenameAction',
444
- },
445
- dynamic: {
446
- use: 'schemaRegistryDynamicAction',
447
- },
448
- },
449
- });
450
-
451
- const registry = new FlowSchemaRegistry();
452
- registry.registerActions({
453
- schemaRegistryRenameAction: {
454
- name: 'schemaRegistryRenameAction',
455
- handler: () => null,
456
- paramsSchema: {
457
- type: 'object',
458
- properties: {
459
- title: {
460
- type: 'string',
461
- },
462
- },
463
- required: ['title'],
464
- additionalProperties: false,
465
- },
466
- },
467
- schemaRegistryDynamicAction: {
468
- name: 'schemaRegistryDynamicAction',
469
- handler: () => null,
470
- uiSchema: (() => ({
471
- enabled: {
472
- type: 'boolean',
473
- },
474
- })) as any,
475
- },
476
- });
477
- registry.registerModels({
478
- SchemaRegistryChildModel,
479
- SchemaRegistryParentModel,
480
- });
481
-
482
- const doc = registry.getModelDocument('SchemaRegistryParentModel');
483
-
484
- expect((doc.jsonSchema.properties?.subModels as any)?.properties?.header).toMatchObject({
485
- type: 'object',
486
- properties: {
487
- use: {
488
- const: 'SchemaRegistryChildModel',
489
- },
490
- },
491
- });
492
- expect((doc.jsonSchema.properties?.subModels as any)?.properties?.items).toMatchObject({
493
- type: 'array',
494
- });
495
- expect((doc.jsonSchema.properties?.stepParams as any)?.properties?.settings?.properties?.rename).toMatchObject({
496
- properties: {
497
- title: {
498
- type: 'string',
499
- },
500
- },
501
- required: ['title'],
502
- additionalProperties: false,
503
- });
504
- expect(doc.dynamicHints).toEqual(
505
- expect.arrayContaining([
506
- expect.objectContaining({
507
- kind: 'manual-schema-required',
508
- path: 'SchemaRegistryParentModel.subModels.header',
509
- 'x-flow': expect.objectContaining({
510
- slotRules: expect.objectContaining({
511
- slotKey: 'header',
512
- type: 'object',
513
- allowedUses: ['SchemaRegistryChildModel'],
514
- }),
515
- }),
516
- }),
517
- expect.objectContaining({
518
- kind: 'manual-schema-required',
519
- path: 'SchemaRegistryParentModel.subModels.items',
520
- 'x-flow': expect.objectContaining({
521
- slotRules: expect.objectContaining({
522
- slotKey: 'items',
523
- type: 'array',
524
- allowedUses: ['SchemaRegistryChildModel'],
525
- }),
526
- }),
527
- }),
528
- expect.objectContaining({
529
- kind: 'dynamic-ui-schema',
530
- path: 'actions.schemaRegistryDynamicAction',
531
- 'x-flow': expect.objectContaining({
532
- unresolvedReason: 'function-ui-schema',
533
- }),
534
- }),
535
- ]),
536
- );
537
- expect(doc.coverage.status).toBe('mixed');
538
- });
539
-
540
- it('should emit dynamic child hints for function-based model metadata', () => {
541
- class SchemaRegistryDynamicModel extends FlowModel {}
542
-
543
- SchemaRegistryDynamicModel.define({
544
- label: 'Schema dynamic',
545
- children: (async () => []) as any,
546
- createModelOptions: (() => ({
547
- use: 'SchemaRegistryDynamicModel',
548
- })) as any,
549
- });
550
-
551
- const registry = new FlowSchemaRegistry();
552
- registry.registerModels({
553
- SchemaRegistryDynamicModel,
554
- });
555
-
556
- const doc = registry.getModelDocument('SchemaRegistryDynamicModel');
557
-
558
- expect(doc.dynamicHints).toEqual(
559
- expect.arrayContaining([
560
- expect.objectContaining({
561
- kind: 'dynamic-children',
562
- path: 'SchemaRegistryDynamicModel.meta.children',
563
- }),
564
- expect.objectContaining({
565
- kind: 'dynamic-children',
566
- path: 'SchemaRegistryDynamicModel.meta.createModelOptions',
567
- }),
568
- ]),
569
- );
570
- });
571
-
572
- it('should preserve nested grid layout step params schema in model documents', () => {
573
- const registry = new FlowSchemaRegistry();
574
-
575
- registry.registerModelContribution({
576
- use: 'SchemaRegistryGridModel',
577
- stepParamsSchema: {
578
- type: 'object',
579
- properties: {
580
- gridSettings: {
581
- type: 'object',
582
- properties: {
583
- grid: {
584
- type: 'object',
585
- properties: {
586
- rows: {
587
- type: 'object',
588
- additionalProperties: {
589
- type: 'array',
590
- items: {
591
- type: 'array',
592
- items: {
593
- type: 'string',
594
- },
595
- },
596
- },
597
- },
598
- sizes: {
599
- type: 'object',
600
- additionalProperties: {
601
- type: 'array',
602
- items: {
603
- type: 'number',
604
- },
605
- },
606
- },
607
- rowOrder: {
608
- type: 'array',
609
- items: {
610
- type: 'string',
611
- },
612
- },
613
- },
614
- additionalProperties: false,
615
- },
616
- },
617
- additionalProperties: true,
618
- },
619
- },
620
- additionalProperties: true,
621
- },
622
- });
623
-
624
- const doc = registry.getModelDocument('SchemaRegistryGridModel');
625
-
626
- expectGridLayoutSchemaDocument(doc);
627
- });
628
-
629
- it('should build bundle-friendly documents from pure data contributions', () => {
630
- const registry = new FlowSchemaRegistry();
631
-
632
- registry.registerActionContribution({
633
- name: 'schemaRegistryContributionAction',
634
- title: 'Contribution action',
635
- paramsSchema: {
636
- type: 'object',
637
- properties: {
638
- enabled: {
639
- type: 'boolean',
640
- },
641
- },
642
- additionalProperties: false,
643
- },
644
- docs: {
645
- minimalExample: {
646
- enabled: true,
647
- },
648
- },
649
- });
650
-
651
- registry.registerModelContribution({
652
- use: 'SchemaRegistryContributionModel',
653
- title: 'Contribution model',
654
- stepParamsSchema: {
655
- type: 'object',
656
- properties: {
657
- settings: {
658
- type: 'object',
659
- properties: {
660
- title: {
661
- type: 'string',
662
- },
663
- toggle: {
664
- type: 'object',
665
- properties: {
666
- enabled: {
667
- type: 'boolean',
668
- },
669
- },
670
- additionalProperties: false,
671
- },
672
- },
673
- required: ['title'],
674
- additionalProperties: false,
675
- },
676
- },
677
- additionalProperties: true,
678
- },
679
- subModelSlots: {
680
- body: {
681
- type: 'array',
682
- uses: ['SchemaRegistryChildModel'],
683
- schema: {
684
- type: 'object',
685
- required: ['uid', 'use'],
686
- properties: {
687
- uid: { type: 'string' },
688
- use: { type: 'string' },
689
- },
690
- additionalProperties: true,
691
- },
692
- },
693
- },
694
- docs: {
695
- minimalExample: {
696
- uid: 'contribution-model-1',
697
- use: 'SchemaRegistryContributionModel',
698
- stepParams: {
699
- settings: {
700
- title: 'Contribution model',
701
- },
702
- },
703
- },
704
- commonPatterns: [
705
- {
706
- title: 'Minimal contribution model',
707
- snippet: {
708
- stepParams: {
709
- settings: {
710
- title: 'Contribution model',
711
- },
712
- },
713
- },
714
- },
715
- ],
716
- antiPatterns: [
717
- {
718
- title: 'Missing title',
719
- },
720
- ],
721
- dynamicHints: [
722
- {
723
- kind: 'dynamic-children',
724
- path: 'SchemaRegistryContributionModel.subModels.body',
725
- message: 'body slot is curated manually.',
726
- 'x-flow': {
727
- slotRules: {
728
- slotKey: 'body',
729
- type: 'array',
730
- allowedUses: ['SchemaRegistryChildModel'],
731
- },
732
- },
733
- },
734
- ],
735
- },
736
- });
737
-
738
- const doc = registry.getModelDocument('SchemaRegistryContributionModel');
739
- const bundle = registry.getSchemaBundle(['SchemaRegistryContributionModel']);
740
-
741
- expect(doc.minimalExample).toMatchObject({
742
- use: 'SchemaRegistryContributionModel',
743
- });
744
- expect(doc.skeleton).toMatchObject({
745
- uid: 'todo-uid',
746
- use: 'SchemaRegistryContributionModel',
747
- });
748
- expect(doc.commonPatterns).toEqual(
749
- expect.arrayContaining([
750
- expect.objectContaining({
751
- title: 'Minimal contribution model',
752
- }),
753
- ]),
754
- );
755
- expect(doc.dynamicHints).toEqual(
756
- expect.arrayContaining([
757
- expect.objectContaining({
758
- path: 'SchemaRegistryContributionModel.subModels.body',
759
- 'x-flow': expect.objectContaining({
760
- slotRules: expect.objectContaining({
761
- allowedUses: ['SchemaRegistryChildModel'],
762
- }),
763
- }),
764
- }),
765
- ]),
766
- );
767
- expect(bundle).not.toHaveProperty('generatedAt');
768
- expect(bundle).not.toHaveProperty('summary');
769
- expect(bundle.items[0]).toMatchObject({
770
- use: 'SchemaRegistryContributionModel',
771
- subModelCatalog: {
772
- body: expect.objectContaining({
773
- type: 'array',
774
- candidates: [expect.objectContaining({ use: 'SchemaRegistryChildModel' })],
775
- }),
776
- },
777
- });
778
- expect(
779
- Object.keys(bundle.items[0] || {}).filter((key) => !['use', 'title', 'subModelCatalog'].includes(key)),
780
- ).toEqual([]);
781
- });
782
-
783
- it('should truncate recursive ancestor snapshots in model documents', () => {
784
- const registry = new FlowSchemaRegistry();
785
-
786
- registry.registerModelContribution({
787
- use: 'SchemaRegistryLoopRootModel',
788
- subModelSlots: {
789
- actions: {
790
- type: 'array',
791
- uses: ['SchemaRegistryLoopActionModel'],
792
- },
793
- },
794
- });
795
- registry.registerModelContribution({
796
- use: 'SchemaRegistryLoopActionModel',
797
- subModelSlots: {
798
- page: {
799
- type: 'object',
800
- use: 'SchemaRegistryLoopPageModel',
801
- },
802
- },
803
- });
804
- registry.registerModelContribution({
805
- use: 'SchemaRegistryLoopPageModel',
806
- subModelSlots: {
807
- tabs: {
808
- type: 'array',
809
- uses: ['SchemaRegistryLoopTabModel'],
810
- },
811
- },
812
- });
813
- registry.registerModelContribution({
814
- use: 'SchemaRegistryLoopTabModel',
815
- subModelSlots: {
816
- grid: {
817
- type: 'object',
818
- use: 'SchemaRegistryLoopGridModel',
819
- },
820
- },
821
- });
822
- registry.registerModelContribution({
823
- use: 'SchemaRegistryLoopGridModel',
824
- subModelSlots: {
825
- items: {
826
- type: 'array',
827
- uses: ['SchemaRegistryLoopRootModel'],
828
- },
829
- },
830
- });
831
-
832
- const doc = registry.getModelDocument('SchemaRegistryLoopRootModel');
833
- const rootSubModels = (doc.jsonSchema.properties?.subModels as any)?.properties;
834
- const actionNode = rootSubModels?.actions?.items;
835
- const actionSubModels = actionNode?.properties?.subModels?.properties;
836
- const pageNode = actionSubModels?.page;
837
- const pageSubModels = pageNode?.properties?.subModels?.properties;
838
- const tabNode = pageSubModels?.tabs?.items;
839
- const tabSubModels = tabNode?.properties?.subModels?.properties;
840
- const gridNode = tabSubModels?.grid;
841
- const gridSubModels = gridNode?.properties?.subModels?.properties;
842
- const recursiveRoot = gridSubModels?.items?.items;
843
-
844
- expect(recursiveRoot).toMatchObject({
845
- type: 'object',
846
- properties: {
847
- use: {
848
- const: 'SchemaRegistryLoopRootModel',
849
- },
850
- subModels: {
851
- type: 'object',
852
- additionalProperties: true,
853
- },
854
- },
855
- });
856
- expect((recursiveRoot?.properties?.subModels as any)?.properties).toBeUndefined();
857
- });
858
-
859
- it('should treat abstract models as non-queryable while allowing explicit internal concrete models', () => {
860
- const registry = new FlowSchemaRegistry();
861
-
862
- registry.registerModelContribution({
863
- use: 'SchemaRegistryInternalBaseModel',
864
- exposure: 'internal',
865
- abstract: true,
866
- allowDirectUse: false,
867
- suggestedUses: ['SchemaRegistryPublicModel'],
868
- });
869
- registry.registerModelContribution({
870
- use: 'SchemaRegistryInternalConcreteModel',
871
- exposure: 'internal',
872
- stepParamsSchema: {
873
- type: 'object',
874
- properties: {
875
- enabled: {
876
- type: 'boolean',
877
- },
878
- },
879
- additionalProperties: true,
880
- },
881
- });
882
- registry.registerModelContribution({
883
- use: 'SchemaRegistryPublicModel',
884
- stepParamsSchema: {
885
- type: 'object',
886
- properties: {
887
- settings: {
888
- type: 'object',
889
- additionalProperties: true,
890
- },
891
- },
892
- additionalProperties: true,
893
- },
894
- });
895
-
896
- expect(registry.hasQueryableModel('SchemaRegistryInternalBaseModel')).toBe(false);
897
- expect(registry.hasQueryableModel('SchemaRegistryInternalConcreteModel')).toBe(true);
898
- expect(registry.isDirectUseAllowed('SchemaRegistryInternalBaseModel')).toBe(false);
899
- expect(registry.isDirectUseAllowed('SchemaRegistryInternalConcreteModel')).toBe(true);
900
- expect(registry.getSuggestedUses('SchemaRegistryInternalBaseModel')).toEqual(['SchemaRegistryPublicModel']);
901
- expect(registry.listModelUses({ publicOnly: true })).toEqual(['SchemaRegistryPublicModel']);
902
- expect(registry.getSchemaBundle().items).toEqual([]);
903
- expect(registry.getSchemaBundle(['SchemaRegistryInternalConcreteModel']).items.map((item) => item.use)).toEqual([
904
- 'SchemaRegistryInternalConcreteModel',
905
- ]);
906
- });
907
-
908
- it('should resolve direct child schema patches by parent slot context', () => {
909
- const registry = new FlowSchemaRegistry();
910
-
911
- registry.registerModelContribution({
912
- use: 'SchemaRegistryContextChildModel',
913
- stepParamsSchema: {
914
- type: 'object',
915
- properties: {
916
- shared: {
917
- type: 'string',
918
- },
919
- },
920
- additionalProperties: true,
921
- },
922
- source: 'official',
923
- strict: true,
924
- });
925
-
926
- registry.registerModelContribution({
927
- use: 'SchemaRegistryParentAlphaModel',
928
- source: 'official',
929
- strict: true,
930
- subModelSlots: {
931
- body: {
932
- type: 'object',
933
- use: 'SchemaRegistryContextChildModel',
934
- childSchemaPatch: {
935
- stepParamsSchema: {
936
- type: 'object',
937
- properties: {
938
- alpha: {
939
- type: 'string',
940
- },
941
- },
942
- required: ['alpha'],
943
- additionalProperties: false,
944
- },
945
- },
946
- },
947
- },
948
- });
949
-
950
- registry.registerModelContribution({
951
- use: 'SchemaRegistryParentBetaModel',
952
- source: 'official',
953
- strict: true,
954
- subModelSlots: {
955
- body: {
956
- type: 'object',
957
- use: 'SchemaRegistryContextChildModel',
958
- childSchemaPatch: {
959
- stepParamsSchema: {
960
- type: 'object',
961
- properties: {
962
- beta: {
963
- type: 'number',
964
- },
965
- },
966
- required: ['beta'],
967
- additionalProperties: false,
968
- },
969
- },
970
- },
971
- },
972
- });
973
-
974
- expect(
975
- registry.getModelDocument('SchemaRegistryContextChildModel').jsonSchema.properties?.stepParams,
976
- ).toMatchObject({
977
- properties: {
978
- shared: {
979
- type: 'string',
980
- },
981
- },
982
- additionalProperties: true,
983
- });
984
-
985
- expect(
986
- registry.resolveModelSchema('SchemaRegistryContextChildModel', [
987
- {
988
- parentUse: 'SchemaRegistryParentAlphaModel',
989
- slotKey: 'body',
990
- childUse: 'SchemaRegistryContextChildModel',
991
- },
992
- ]).stepParamsSchema,
993
- ).toMatchObject({
994
- properties: {
995
- shared: {
996
- type: 'string',
997
- },
998
- alpha: {
999
- type: 'string',
1000
- },
1001
- },
1002
- required: ['alpha'],
1003
- additionalProperties: false,
1004
- });
1005
-
1006
- expect(
1007
- (registry.getModelDocument('SchemaRegistryParentAlphaModel').jsonSchema.properties?.subModels as any)?.properties
1008
- ?.body?.properties?.stepParams,
1009
- ).toMatchObject({
1010
- properties: {
1011
- alpha: {
1012
- type: 'string',
1013
- },
1014
- },
1015
- required: ['alpha'],
1016
- additionalProperties: false,
1017
- });
1018
-
1019
- expect(
1020
- (registry.getModelDocument('SchemaRegistryParentBetaModel').jsonSchema.properties?.subModels as any)?.properties
1021
- ?.body?.properties?.stepParams,
1022
- ).toMatchObject({
1023
- properties: {
1024
- beta: {
1025
- type: 'number',
1026
- },
1027
- },
1028
- required: ['beta'],
1029
- additionalProperties: false,
1030
- });
1031
-
1032
- const bundle = registry.getSchemaBundle(['SchemaRegistryParentAlphaModel', 'SchemaRegistryParentBetaModel']);
1033
- const alphaItem = bundle.items.find((item) => item.use === 'SchemaRegistryParentAlphaModel');
1034
- const betaItem = bundle.items.find((item) => item.use === 'SchemaRegistryParentBetaModel');
1035
-
1036
- expect(alphaItem?.subModelCatalog?.body?.candidates?.map((item) => item.use)).toEqual([
1037
- 'SchemaRegistryContextChildModel',
1038
- ]);
1039
- expect(betaItem?.subModelCatalog?.body?.candidates?.map((item) => item.use)).toEqual([
1040
- 'SchemaRegistryContextChildModel',
1041
- ]);
1042
- });
1043
-
1044
- it('should apply ancestor descendant patches before direct child patches', () => {
1045
- const registry = new FlowSchemaRegistry();
1046
-
1047
- registry.registerModelContribution({
1048
- use: 'SchemaRegistryDescLeafModel',
1049
- stepParamsSchema: {
1050
- type: 'object',
1051
- additionalProperties: true,
1052
- },
1053
- source: 'official',
1054
- strict: true,
1055
- });
1056
-
1057
- registry.registerModelContribution({
1058
- use: 'SchemaRegistryDescParentModel',
1059
- source: 'official',
1060
- strict: true,
1061
- subModelSlots: {
1062
- section: {
1063
- type: 'object',
1064
- use: 'SchemaRegistryDescBridgeModel',
1065
- childSchemaPatch: {
1066
- subModelSlots: {
1067
- leaf: {
1068
- type: 'object',
1069
- use: 'SchemaRegistryDescLeafModel',
1070
- childSchemaPatch: {
1071
- stepParamsSchema: {
1072
- type: 'object',
1073
- properties: {
1074
- marker: {
1075
- type: 'string',
1076
- },
1077
- directOnly: {
1078
- type: 'string',
1079
- },
1080
- },
1081
- required: ['directOnly'],
1082
- additionalProperties: false,
1083
- },
1084
- },
1085
- },
1086
- },
1087
- },
1088
- descendantSchemaPatches: [
1089
- {
1090
- path: [
1091
- {
1092
- slotKey: 'leaf',
1093
- use: 'SchemaRegistryDescLeafModel',
1094
- },
1095
- ],
1096
- patch: {
1097
- stepParamsSchema: {
1098
- type: 'object',
1099
- properties: {
1100
- marker: {
1101
- type: 'number',
1102
- },
1103
- ancestorOnly: {
1104
- type: 'boolean',
1105
- },
1106
- },
1107
- required: ['ancestorOnly'],
1108
- additionalProperties: true,
1109
- },
1110
- },
1111
- },
1112
- ],
1113
- },
1114
- },
1115
- });
1116
-
1117
- const resolved = registry.resolveModelSchema('SchemaRegistryDescLeafModel', [
1118
- {
1119
- parentUse: 'SchemaRegistryDescParentModel',
1120
- slotKey: 'section',
1121
- childUse: 'SchemaRegistryDescBridgeModel',
1122
- },
1123
- {
1124
- parentUse: 'SchemaRegistryDescBridgeModel',
1125
- slotKey: 'leaf',
1126
- childUse: 'SchemaRegistryDescLeafModel',
1127
- },
1128
- ]);
1129
-
1130
- expect(resolved.stepParamsSchema).toMatchObject({
1131
- properties: {
1132
- ancestorOnly: {
1133
- type: 'boolean',
1134
- },
1135
- directOnly: {
1136
- type: 'string',
1137
- },
1138
- marker: {
1139
- type: 'string',
1140
- },
1141
- },
1142
- required: ['directOnly'],
1143
- additionalProperties: false,
1144
- });
1145
-
1146
- expect(
1147
- (registry.getModelDocument('SchemaRegistryDescParentModel').jsonSchema.properties?.subModels as any)?.properties
1148
- ?.section?.properties?.subModels?.properties?.leaf?.properties?.stepParams,
1149
- ).toMatchObject({
1150
- properties: {
1151
- ancestorOnly: {
1152
- type: 'boolean',
1153
- },
1154
- directOnly: {
1155
- type: 'string',
1156
- },
1157
- },
1158
- required: ['directOnly'],
1159
- additionalProperties: false,
1160
- });
1161
- });
1162
-
1163
- it('should only use legacy slot schema as fallback when no child use can be resolved', () => {
1164
- const registry = new FlowSchemaRegistry();
1165
-
1166
- registry.registerModelContribution({
1167
- use: 'SchemaRegistryLegacyChildModel',
1168
- stepParamsSchema: {
1169
- type: 'object',
1170
- properties: {
1171
- title: {
1172
- type: 'string',
1173
- },
1174
- },
1175
- required: ['title'],
1176
- additionalProperties: false,
1177
- },
1178
- });
1179
-
1180
- registry.registerModelContribution({
1181
- use: 'SchemaRegistryLegacyParentModel',
1182
- subModelSlots: {
1183
- body: {
1184
- type: 'object',
1185
- use: 'SchemaRegistryLegacyChildModel',
1186
- schema: {
1187
- type: 'object',
1188
- required: ['uid', 'use'],
1189
- properties: {
1190
- uid: { type: 'string' },
1191
- use: { type: 'string' },
1192
- },
1193
- additionalProperties: true,
1194
- },
1195
- },
1196
- },
1197
- });
1198
-
1199
- expect(
1200
- (registry.getModelDocument('SchemaRegistryLegacyParentModel').jsonSchema.properties?.subModels as any)?.properties
1201
- ?.body,
1202
- ).toMatchObject({
1203
- anyOf: expect.arrayContaining([
1204
- expect.objectContaining({
1205
- properties: expect.objectContaining({
1206
- use: {
1207
- const: 'SchemaRegistryLegacyChildModel',
1208
- },
1209
- stepParams: expect.objectContaining({
1210
- required: ['title'],
1211
- additionalProperties: false,
1212
- }),
1213
- }),
1214
- }),
1215
- expect.objectContaining({
1216
- properties: expect.objectContaining({
1217
- use: {
1218
- type: 'string',
1219
- },
1220
- }),
1221
- }),
1222
- ]),
1223
- });
1224
- });
1225
-
1226
- it('should expose anonymous child snapshot patches when slot use is unresolved', () => {
1227
- const registry = new FlowSchemaRegistry();
1228
-
1229
- registry.registerModelContribution({
1230
- use: 'SchemaRegistryAnonymousGridModel',
1231
- stepParamsSchema: {
1232
- type: 'object',
1233
- properties: {
1234
- title: {
1235
- type: 'string',
1236
- },
1237
- },
1238
- required: ['title'],
1239
- additionalProperties: false,
1240
- },
1241
- });
1242
-
1243
- registry.registerModelContribution({
1244
- use: 'SchemaRegistryAnonymousParentModel',
1245
- subModelSlots: {
1246
- body: {
1247
- type: 'object',
1248
- childSchemaPatch: {
1249
- stepParamsSchema: {
1250
- type: 'object',
1251
- properties: {
1252
- mode: {
1253
- type: 'string',
1254
- enum: ['compact', 'full'],
1255
- },
1256
- },
1257
- required: ['mode'],
1258
- additionalProperties: false,
1259
- },
1260
- subModelSlots: {
1261
- grid: {
1262
- type: 'object',
1263
- use: 'SchemaRegistryAnonymousGridModel',
1264
- },
1265
- },
1266
- },
1267
- },
1268
- },
1269
- });
1270
-
1271
- expect(
1272
- (registry.getModelDocument('SchemaRegistryAnonymousParentModel').jsonSchema.properties?.subModels as any)
1273
- ?.properties?.body,
1274
- ).toMatchObject({
1275
- properties: {
1276
- use: {
1277
- type: 'string',
1278
- },
1279
- stepParams: {
1280
- properties: {
1281
- mode: {
1282
- type: 'string',
1283
- enum: ['compact', 'full'],
1284
- },
1285
- },
1286
- required: ['mode'],
1287
- additionalProperties: false,
1288
- },
1289
- subModels: {
1290
- properties: {
1291
- grid: {
1292
- properties: {
1293
- use: {
1294
- const: 'SchemaRegistryAnonymousGridModel',
1295
- },
1296
- stepParams: {
1297
- required: ['title'],
1298
- additionalProperties: false,
1299
- },
1300
- },
1301
- },
1302
- },
1303
- },
1304
- },
1305
- });
1306
-
1307
- const bundle = registry.getSchemaBundle(['SchemaRegistryAnonymousParentModel']);
1308
- expect(bundle.items[0]).toMatchObject({
1309
- use: 'SchemaRegistryAnonymousParentModel',
1310
- subModelCatalog: {
1311
- body: {
1312
- type: 'object',
1313
- open: true,
1314
- candidates: [],
1315
- },
1316
- },
1317
- });
1318
- });
1319
-
1320
- it('should expose recursive sub-model catalogs for internal descendants without promoting them to top-level items', () => {
1321
- const registry = new FlowSchemaRegistry();
1322
-
1323
- registry.registerModelContribution(
1324
- internalModelContribution('SchemaRegistryBundleLeafModel', 'bundle-leaf-uid', {
1325
- allowDirectUse: false,
1326
- stepParamsSchema: objectSchema(
1327
- {
1328
- label: {
1329
- type: 'string',
1330
- },
1331
- },
1332
- { required: ['label'], additionalProperties: false },
1333
- ),
1334
- }),
1335
- );
1336
-
1337
- registry.registerModelContribution(
1338
- internalModelContribution('SchemaRegistryBundleChildModel', 'bundle-child-uid', {
1339
- allowDirectUse: false,
1340
- skeleton: {
1341
- uid: 'bundle-child-uid',
1342
- use: 'SchemaRegistryBundleChildModel',
1343
- subModels: {
1344
- items: [],
1345
- dynamicZone: [],
1346
- },
1347
- },
1348
- subModelSlots: {
1349
- items: arraySlot({
1350
- uses: ['SchemaRegistryBundleLeafModel'],
1351
- }),
1352
- dynamicZone: arraySlot(),
1353
- },
1354
- }),
1355
- );
1356
-
1357
- registry.registerModelContribution(
1358
- modelContribution('SchemaRegistryBundleParentModel', {
1359
- skeleton: {
1360
- uid: 'bundle-parent-uid',
1361
- use: 'SchemaRegistryBundleParentModel',
1362
- subModels: {
1363
- body: {
1364
- use: 'SchemaRegistryBundleChildModel',
1365
- },
1366
- },
1367
- },
1368
- subModelSlots: {
1369
- body: objectSlot({
1370
- use: 'SchemaRegistryBundleChildModel',
1371
- }),
1372
- },
1373
- }),
1374
- );
1375
-
1376
- const publicBundle = registry.getSchemaBundle();
1377
- const bundle = registry.getSchemaBundle(['SchemaRegistryBundleParentModel']);
1378
- const explicitInternalBundle = registry.getSchemaBundle(['SchemaRegistryBundleChildModel']);
1379
-
1380
- expect(publicBundle.items).toEqual([]);
1381
- expect(explicitInternalBundle.items.map((item) => item.use)).toEqual(['SchemaRegistryBundleChildModel']);
1382
- expect(bundle.items[0]).toMatchObject({
1383
- use: 'SchemaRegistryBundleParentModel',
1384
- subModelCatalog: {
1385
- body: {
1386
- type: 'object',
1387
- candidates: [
1388
- expect.objectContaining({
1389
- use: 'SchemaRegistryBundleChildModel',
1390
- subModelCatalog: {
1391
- items: {
1392
- type: 'array',
1393
- candidates: [expect.objectContaining({ use: 'SchemaRegistryBundleLeafModel' })],
1394
- },
1395
- dynamicZone: {
1396
- type: 'array',
1397
- open: true,
1398
- candidates: [],
1399
- },
1400
- },
1401
- }),
1402
- ],
1403
- },
1404
- },
1405
- });
1406
- });
1407
-
1408
- it('should resolve runtime field binding candidates with compatibility metadata', () => {
1409
- const registry = new FlowSchemaRegistry();
1410
-
1411
- registry.registerFieldBindingContexts([
1412
- { name: 'editable-field' },
1413
- { name: 'display-field' },
1414
- { name: 'filter-field' },
1415
- { name: 'table-column-field', inherits: ['display-field'] },
1416
- { name: 'form-item-field', inherits: ['editable-field'] },
1417
- ]);
1418
-
1419
- registry.registerModelContribution(internalModelContribution('InputFieldModel', 'input-field-uid'));
1420
- registry.registerModelContribution(internalModelContribution('DisplayTextFieldModel', 'display-text-field-uid'));
1421
- registry.registerModelContribution(internalModelContribution('RecordSelectFieldModel', 'record-select-field-uid'));
1422
- registry.registerModelContribution(
1423
- internalModelContribution('CascadeSelectFieldModel', 'cascade-select-field-uid'),
1424
- );
1425
- registry.registerFieldBindings([
1426
- {
1427
- context: 'editable-field',
1428
- use: 'InputFieldModel',
1429
- interfaces: ['input'],
1430
- isDefault: true,
1431
- },
1432
- {
1433
- context: 'display-field',
1434
- use: 'DisplayTextFieldModel',
1435
- interfaces: ['input'],
1436
- isDefault: true,
1437
- },
1438
- {
1439
- context: 'editable-field',
1440
- use: 'RecordSelectFieldModel',
1441
- interfaces: ['m2o'],
1442
- isDefault: true,
1443
- conditions: {
1444
- association: true,
1445
- },
1446
- },
1447
- {
1448
- context: 'editable-field',
1449
- use: 'CascadeSelectFieldModel',
1450
- interfaces: ['m2o'],
1451
- isDefault: true,
1452
- order: 60,
1453
- conditions: {
1454
- association: true,
1455
- targetCollectionTemplateIn: ['tree'],
1456
- },
1457
- },
1458
- ]);
1459
-
1460
- registry.registerModelContribution(
1461
- modelContribution('SchemaRegistryFieldHostModel', {
1462
- skeleton: {
1463
- uid: 'field-host-uid',
1464
- use: 'SchemaRegistryFieldHostModel',
1465
- },
1466
- subModelSlots: {
1467
- field: objectSlot({
1468
- fieldBindingContext: 'form-item-field',
1469
- }),
1470
- },
1471
- }),
1472
- );
1473
- registry.registerModelContribution(
1474
- modelContribution('SchemaRegistryDisplayFieldHostModel', {
1475
- skeleton: {
1476
- uid: 'display-field-host-uid',
1477
- use: 'SchemaRegistryDisplayFieldHostModel',
1478
- },
1479
- subModelSlots: {
1480
- field: objectSlot({
1481
- fieldBindingContext: 'table-column-field',
1482
- }),
1483
- },
1484
- }),
1485
- );
1486
-
1487
- expect(
1488
- registry.resolveFieldBindingCandidates('form-item-field', { interface: 'input' }).map((item) => item.use),
1489
- ).toEqual(['InputFieldModel']);
1490
- expect(
1491
- registry
1492
- .resolveFieldBindingCandidates('form-item-field', {
1493
- interface: 'm2o',
1494
- association: true,
1495
- targetCollectionTemplate: 'tree',
1496
- })
1497
- .map((item) => item.use),
1498
- ).toEqual(['CascadeSelectFieldModel', 'RecordSelectFieldModel']);
1499
-
1500
- const bundle = registry.getSchemaBundle(['SchemaRegistryFieldHostModel', 'SchemaRegistryDisplayFieldHostModel']);
1501
- const formItem = bundle.items.find((item) => item.use === 'SchemaRegistryFieldHostModel');
1502
- const tableItem = bundle.items.find((item) => item.use === 'SchemaRegistryDisplayFieldHostModel');
1503
-
1504
- expect(formItem?.subModelCatalog).toMatchObject({
1505
- field: {
1506
- type: 'object',
1507
- candidates: expect.arrayContaining([
1508
- expect.objectContaining({
1509
- use: 'InputFieldModel',
1510
- compatibility: expect.objectContaining({
1511
- context: 'editable-field',
1512
- interfaces: ['input'],
1513
- isDefault: true,
1514
- inheritParentFieldBinding: true,
1515
- }),
1516
- }),
1517
- expect.objectContaining({
1518
- use: 'RecordSelectFieldModel',
1519
- compatibility: expect.objectContaining({
1520
- context: 'editable-field',
1521
- interfaces: ['m2o'],
1522
- association: true,
1523
- isDefault: true,
1524
- }),
1525
- }),
1526
- ]),
1527
- },
1528
- });
1529
- expect(tableItem?.subModelCatalog).toMatchObject({
1530
- field: {
1531
- type: 'object',
1532
- candidates: [expect.objectContaining({ use: 'DisplayTextFieldModel' })],
1533
- },
1534
- });
1535
-
1536
- const hostDoc = registry.getModelDocument('SchemaRegistryFieldHostModel');
1537
- expect(hostDoc.dynamicHints).toEqual(
1538
- expect.arrayContaining([
1539
- expect.objectContaining({
1540
- path: 'SchemaRegistryFieldHostModel.subModels.field',
1541
- 'x-flow': expect.objectContaining({
1542
- slotRules: expect.objectContaining({
1543
- allowedUses: expect.arrayContaining([
1544
- 'InputFieldModel',
1545
- 'RecordSelectFieldModel',
1546
- 'CascadeSelectFieldModel',
1547
- ]),
1548
- }),
1549
- }),
1550
- }),
1551
- ]),
1552
- );
1553
- expect(JSON.stringify(bundle)).not.toContain('RuntimeFieldModel');
1554
- });
1555
-
1556
- it('should match wildcard field binding interfaces without breaking exact matches', () => {
1557
- const registry = new FlowSchemaRegistry();
1558
-
1559
- registry.registerFieldBindingContexts([{ name: 'editable-field' }]);
1560
- registry.registerModelContribution(internalModelContribution('SchemaRegistryExactFieldModel', 'exact-field-uid'));
1561
- registry.registerModelContribution(
1562
- internalModelContribution('SchemaRegistryWildcardFieldModel', 'wildcard-field-uid'),
1563
- );
1564
-
1565
- registry.registerFieldBindings([
1566
- {
1567
- context: 'editable-field',
1568
- use: 'SchemaRegistryExactFieldModel',
1569
- interfaces: ['input'],
1570
- isDefault: true,
1571
- },
1572
- {
1573
- context: 'editable-field',
1574
- use: 'SchemaRegistryWildcardFieldModel',
1575
- interfaces: ['*'],
1576
- },
1577
- ]);
1578
-
1579
- expect(
1580
- registry.resolveFieldBindingCandidates('editable-field', { interface: 'uuid' }).map((item) => item.use),
1581
- ).toEqual(['SchemaRegistryWildcardFieldModel']);
1582
- expect(
1583
- registry.resolveFieldBindingCandidates('editable-field', { interface: 'input' }).map((item) => item.use),
1584
- ).toEqual(['SchemaRegistryExactFieldModel', 'SchemaRegistryWildcardFieldModel']);
1585
- });
1586
-
1587
- it('should project required and minItems slot constraints into schema documents and bundle catalogs', () => {
1588
- const registry = new FlowSchemaRegistry();
1589
-
1590
- registry.registerModelContribution(
1591
- modelContribution('SchemaRegistryRequiredChildModel', {
1592
- title: 'Required child',
1593
- skeleton: {
1594
- uid: 'required-child',
1595
- use: 'SchemaRegistryRequiredChildModel',
1596
- },
1597
- }),
1598
- );
1599
-
1600
- registry.registerModelContribution(
1601
- modelContribution('SchemaRegistryRequiredParentModel', {
1602
- title: 'Required parent',
1603
- subModelSlots: {
1604
- page: objectSlot({
1605
- use: 'SchemaRegistryRequiredChildModel',
1606
- required: true,
1607
- }),
1608
- tabs: arraySlot({
1609
- uses: ['SchemaRegistryRequiredChildModel'],
1610
- required: true,
1611
- minItems: 1,
1612
- }),
1613
- },
1614
- skeleton: {
1615
- uid: 'required-parent',
1616
- use: 'SchemaRegistryRequiredParentModel',
1617
- subModels: {
1618
- page: {
1619
- uid: 'required-parent-page',
1620
- use: 'SchemaRegistryRequiredChildModel',
1621
- },
1622
- tabs: [
1623
- {
1624
- uid: 'required-parent-tab',
1625
- use: 'SchemaRegistryRequiredChildModel',
1626
- },
1627
- ],
1628
- },
1629
- },
1630
- }),
1631
- );
1632
-
1633
- const doc = registry.getModelDocument('SchemaRegistryRequiredParentModel');
1634
- const bundle = registry.getSchemaBundle(['SchemaRegistryRequiredParentModel']);
1635
- const bundleItem = bundle.items[0];
1636
-
1637
- expect(doc.jsonSchema.properties?.subModels).toMatchObject({
1638
- type: 'object',
1639
- required: ['page', 'tabs'],
1640
- properties: {
1641
- page: {
1642
- type: 'object',
1643
- properties: {
1644
- use: {
1645
- const: 'SchemaRegistryRequiredChildModel',
1646
- },
1647
- },
1648
- },
1649
- tabs: {
1650
- type: 'array',
1651
- minItems: 1,
1652
- },
1653
- },
1654
- });
1655
- expect(bundleItem?.subModelCatalog).toMatchObject({
1656
- page: {
1657
- type: 'object',
1658
- required: true,
1659
- candidates: [expect.objectContaining({ use: 'SchemaRegistryRequiredChildModel' })],
1660
- },
1661
- tabs: {
1662
- type: 'array',
1663
- required: true,
1664
- minItems: 1,
1665
- candidates: [expect.objectContaining({ use: 'SchemaRegistryRequiredChildModel' })],
1666
- },
1667
- });
1668
- });
1669
-
1670
- it('should expose nested popup page slots in schema documents and bundle catalogs', () => {
1671
- const registry = new FlowSchemaRegistry();
1672
-
1673
- registry.registerModelContribution(
1674
- modelContribution('SchemaRegistryPopupGridModel', {
1675
- skeleton: {
1676
- uid: 'popup-grid',
1677
- use: 'SchemaRegistryPopupGridModel',
1678
- },
1679
- }),
1680
- );
1681
-
1682
- registry.registerModelContribution(
1683
- modelContribution('SchemaRegistryPopupTabModel', {
1684
- subModelSlots: {
1685
- grid: objectSlot({
1686
- use: 'SchemaRegistryPopupGridModel',
1687
- required: true,
1688
- }),
1689
- },
1690
- skeleton: {
1691
- uid: 'popup-tab',
1692
- use: 'SchemaRegistryPopupTabModel',
1693
- subModels: {
1694
- grid: {
1695
- uid: 'popup-tab-grid',
1696
- use: 'SchemaRegistryPopupGridModel',
1697
- },
1698
- },
1699
- },
1700
- }),
1701
- );
1702
-
1703
- registry.registerModelContribution(
1704
- modelContribution('SchemaRegistryPopupPageModel', {
1705
- subModelSlots: {
1706
- tabs: arraySlot({
1707
- uses: ['SchemaRegistryPopupTabModel'],
1708
- required: true,
1709
- minItems: 1,
1710
- }),
1711
- },
1712
- skeleton: {
1713
- uid: 'popup-page',
1714
- use: 'SchemaRegistryPopupPageModel',
1715
- subModels: {
1716
- tabs: [
1717
- {
1718
- uid: 'popup-page-tab',
1719
- use: 'SchemaRegistryPopupTabModel',
1720
- subModels: {
1721
- grid: {
1722
- uid: 'popup-page-tab-grid',
1723
- use: 'SchemaRegistryPopupGridModel',
1724
- },
1725
- },
1726
- },
1727
- ],
1728
- },
1729
- },
1730
- }),
1731
- );
1732
-
1733
- registry.registerModelContribution(
1734
- modelContribution('SchemaRegistryPopupActionModel', {
1735
- stepParamsSchema: objectSchema({
1736
- popupSettings: objectSchema(
1737
- {
1738
- openView: objectSchema(
1739
- {
1740
- pageModelClass: {
1741
- type: 'string',
1742
- enum: ['SchemaRegistryPopupPageModel', 'SchemaRegistryRootPageModel'],
1743
- },
1744
- },
1745
- { additionalProperties: false },
1746
- ),
1747
- },
1748
- { additionalProperties: true },
1749
- ),
1750
- }),
1751
- subModelSlots: {
1752
- page: objectSlot({
1753
- use: 'SchemaRegistryPopupPageModel',
1754
- }),
1755
- },
1756
- skeleton: {
1757
- uid: 'popup-action',
1758
- use: 'SchemaRegistryPopupActionModel',
1759
- stepParams: {
1760
- popupSettings: {
1761
- openView: {
1762
- pageModelClass: 'SchemaRegistryPopupPageModel',
1763
- },
1764
- },
1765
- },
1766
- subModels: {
1767
- page: {
1768
- uid: 'popup-action-page',
1769
- use: 'SchemaRegistryPopupPageModel',
1770
- subModels: {
1771
- tabs: [
1772
- {
1773
- uid: 'popup-action-page-tab',
1774
- use: 'SchemaRegistryPopupTabModel',
1775
- subModels: {
1776
- grid: {
1777
- uid: 'popup-action-page-grid',
1778
- use: 'SchemaRegistryPopupGridModel',
1779
- },
1780
- },
1781
- },
1782
- ],
1783
- },
1784
- },
1785
- },
1786
- },
1787
- }),
1788
- );
1789
-
1790
- const doc = registry.getModelDocument('SchemaRegistryPopupActionModel');
1791
- const bundle = registry.getSchemaBundle(['SchemaRegistryPopupActionModel']);
1792
- const item = bundle.items[0];
1793
-
1794
- expect((doc.jsonSchema.properties?.subModels as any)?.properties?.page).toMatchObject({
1795
- type: 'object',
1796
- properties: {
1797
- use: {
1798
- const: 'SchemaRegistryPopupPageModel',
1799
- },
1800
- },
1801
- });
1802
- expect(
1803
- (doc.jsonSchema.properties?.stepParams as any)?.properties?.popupSettings?.properties?.openView,
1804
- ).toMatchObject({
1805
- type: 'object',
1806
- properties: {
1807
- pageModelClass: {
1808
- type: 'string',
1809
- enum: ['SchemaRegistryPopupPageModel', 'SchemaRegistryRootPageModel'],
1810
- },
1811
- },
1812
- });
1813
- expect(doc.minimalExample).toMatchObject({
1814
- use: 'SchemaRegistryPopupActionModel',
1815
- subModels: {
1816
- page: {
1817
- use: 'SchemaRegistryPopupPageModel',
1818
- },
1819
- },
1820
- stepParams: {
1821
- popupSettings: {
1822
- openView: {
1823
- pageModelClass: 'SchemaRegistryPopupPageModel',
1824
- },
1825
- },
1826
- },
1827
- });
1828
- expect(item?.subModelCatalog).toMatchObject({
1829
- page: {
1830
- type: 'object',
1831
- candidates: [
1832
- expect.objectContaining({
1833
- use: 'SchemaRegistryPopupPageModel',
1834
- subModelCatalog: {
1835
- tabs: {
1836
- type: 'array',
1837
- required: true,
1838
- minItems: 1,
1839
- candidates: [
1840
- expect.objectContaining({
1841
- use: 'SchemaRegistryPopupTabModel',
1842
- subModelCatalog: {
1843
- grid: {
1844
- type: 'object',
1845
- required: true,
1846
- candidates: [expect.objectContaining({ use: 'SchemaRegistryPopupGridModel' })],
1847
- },
1848
- },
1849
- }),
1850
- ],
1851
- },
1852
- },
1853
- }),
1854
- ],
1855
- },
1856
- });
1857
- });
1858
-
1859
- it('should expose compact public documents without recursive schema or nested hints by default', () => {
1860
- const registry = new FlowSchemaRegistry();
1861
-
1862
- registry.registerModelContribution(
1863
- modelContribution('SchemaRegistryCompactLeafModel', {
1864
- stepParamsSchema: objectSchema(
1865
- {
1866
- leaf: { type: 'string' },
1867
- },
1868
- { additionalProperties: false },
1869
- ),
1870
- dynamicHints: [
1871
- {
1872
- kind: 'dynamic-ui-schema',
1873
- path: 'SchemaRegistryCompactLeafModel.stepParams.leaf',
1874
- message: 'Leaf settings are dynamic.',
1875
- },
1876
- ],
1877
- }),
1878
- );
1879
-
1880
- registry.registerModelContribution(
1881
- modelContribution('SchemaRegistryCompactChildModel', {
1882
- stepParamsSchema: objectSchema(
1883
- {
1884
- child: { type: 'string' },
1885
- },
1886
- { additionalProperties: false },
1887
- ),
1888
- dynamicHints: [
1889
- {
1890
- kind: 'dynamic-ui-schema',
1891
- path: 'SchemaRegistryCompactChildModel.stepParams.child',
1892
- message: 'Child settings are dynamic.',
1893
- },
1894
- ],
1895
- subModelSlots: {
1896
- footer: objectSlot({
1897
- use: 'SchemaRegistryCompactLeafModel',
1898
- }),
1899
- },
1900
- }),
1901
- );
1902
-
1903
- registry.registerModelContribution(
1904
- modelContribution('SchemaRegistryCompactParentModel', {
1905
- stepParamsSchema: objectSchema(
1906
- {
1907
- enabled: { type: 'boolean' },
1908
- },
1909
- { additionalProperties: false },
1910
- ),
1911
- subModelSlots: {
1912
- body: objectSlot({
1913
- use: 'SchemaRegistryCompactChildModel',
1914
- required: true,
1915
- }),
1916
- },
1917
- }),
1918
- );
1919
-
1920
- const compact = registry.getPublicModelDocument('SchemaRegistryCompactParentModel');
1921
- const full = registry.getPublicModelDocument('SchemaRegistryCompactParentModel', { detail: 'full' });
1922
-
1923
- expect(compact).not.toHaveProperty('coverage');
1924
- expect(compact).not.toHaveProperty('skeleton');
1925
- expect(compact).not.toHaveProperty('examples');
1926
- expect(compact.dynamicHints).toEqual(
1927
- expect.arrayContaining([
1928
- expect.objectContaining({
1929
- path: 'SchemaRegistryCompactParentModel.subModels.body',
1930
- }),
1931
- ]),
1932
- );
1933
- expect(JSON.stringify(compact.dynamicHints)).not.toContain('SchemaRegistryCompactChildModel.stepParams.child');
1934
- expect(
1935
- (compact.jsonSchema.properties?.subModels as any)?.properties?.body?.properties?.subModels?.properties?.footer,
1936
- ).toBeUndefined();
1937
- expect((full.dynamicHints || []).length).toBeGreaterThan(compact.dynamicHints.length);
1938
- expect(
1939
- (full.jsonSchema.properties?.subModels as any)?.properties?.body?.properties?.subModels?.properties,
1940
- ).toMatchObject({
1941
- footer: {
1942
- type: 'object',
1943
- properties: {
1944
- use: {
1945
- const: 'SchemaRegistryCompactLeafModel',
1946
- },
1947
- },
1948
- },
1949
- });
1950
- });
1951
- });