@api-client/core 0.18.17 → 0.18.19

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 (75) hide show
  1. package/build/src/{modeling → decorators}/observed.d.ts +3 -3
  2. package/build/src/decorators/observed.d.ts.map +1 -0
  3. package/build/src/{modeling → decorators}/observed.js +4 -4
  4. package/build/src/decorators/observed.js.map +1 -0
  5. package/build/src/modeling/ApiModel.js +1 -1
  6. package/build/src/modeling/ApiModel.js.map +1 -1
  7. package/build/src/modeling/DataDomain.d.ts +35 -1
  8. package/build/src/modeling/DataDomain.d.ts.map +1 -1
  9. package/build/src/modeling/DataDomain.js +120 -0
  10. package/build/src/modeling/DataDomain.js.map +1 -1
  11. package/build/src/modeling/DomainAssociation.d.ts +7 -0
  12. package/build/src/modeling/DomainAssociation.d.ts.map +1 -1
  13. package/build/src/modeling/DomainAssociation.js +44 -1
  14. package/build/src/modeling/DomainAssociation.js.map +1 -1
  15. package/build/src/modeling/DomainEntity.d.ts +6 -0
  16. package/build/src/modeling/DomainEntity.d.ts.map +1 -1
  17. package/build/src/modeling/DomainEntity.js +21 -1
  18. package/build/src/modeling/DomainEntity.js.map +1 -1
  19. package/build/src/modeling/DomainModel.js +1 -1
  20. package/build/src/modeling/DomainModel.js.map +1 -1
  21. package/build/src/modeling/DomainNamespace.js +1 -1
  22. package/build/src/modeling/DomainNamespace.js.map +1 -1
  23. package/build/src/modeling/DomainProperty.d.ts +15 -0
  24. package/build/src/modeling/DomainProperty.d.ts.map +1 -1
  25. package/build/src/modeling/DomainProperty.js +64 -3
  26. package/build/src/modeling/DomainProperty.js.map +1 -1
  27. package/build/src/modeling/DomainSerialization.d.ts.map +1 -1
  28. package/build/src/modeling/DomainSerialization.js +2 -2
  29. package/build/src/modeling/DomainSerialization.js.map +1 -1
  30. package/build/src/modeling/definitions/SKU.d.ts.map +1 -1
  31. package/build/src/modeling/definitions/SKU.js +2 -0
  32. package/build/src/modeling/definitions/SKU.js.map +1 -1
  33. package/build/src/modeling/helpers/Intelisense.d.ts +472 -0
  34. package/build/src/modeling/helpers/Intelisense.d.ts.map +1 -0
  35. package/build/src/modeling/helpers/Intelisense.js +1200 -0
  36. package/build/src/modeling/helpers/Intelisense.js.map +1 -0
  37. package/build/src/modeling/templates/blog-domain.d.ts +40 -0
  38. package/build/src/modeling/templates/blog-domain.d.ts.map +1 -0
  39. package/build/src/modeling/templates/blog-domain.js +621 -0
  40. package/build/src/modeling/templates/blog-domain.js.map +1 -0
  41. package/build/src/modeling/templates/ecommerce-domain.d.ts +39 -0
  42. package/build/src/modeling/templates/ecommerce-domain.d.ts.map +1 -0
  43. package/build/src/modeling/templates/ecommerce-domain.js +663 -0
  44. package/build/src/modeling/templates/ecommerce-domain.js.map +1 -0
  45. package/build/src/modeling/types.d.ts +49 -0
  46. package/build/src/modeling/types.d.ts.map +1 -1
  47. package/build/src/modeling/types.js.map +1 -1
  48. package/build/src/models/Thing.js +1 -1
  49. package/build/src/models/Thing.js.map +1 -1
  50. package/build/tsconfig.tsbuildinfo +1 -1
  51. package/data/models/example-generator-api.json +6 -6
  52. package/package.json +2 -1
  53. package/src/{modeling → decorators}/observed.ts +5 -5
  54. package/src/modeling/ApiModel.ts +1 -1
  55. package/src/modeling/DataDomain.ts +144 -0
  56. package/src/modeling/DomainAssociation.ts +51 -1
  57. package/src/modeling/DomainEntity.ts +24 -1
  58. package/src/modeling/DomainModel.ts +1 -1
  59. package/src/modeling/DomainNamespace.ts +1 -1
  60. package/src/modeling/DomainProperty.ts +66 -1
  61. package/src/modeling/DomainSerialization.ts +2 -4
  62. package/src/modeling/definitions/SKU.ts +2 -0
  63. package/src/modeling/helpers/Intelisense.ts +1345 -0
  64. package/src/modeling/templates/blog-domain.ts +787 -0
  65. package/src/modeling/templates/ecommerce-domain.ts +834 -0
  66. package/src/modeling/types.ts +63 -0
  67. package/src/models/Thing.ts +1 -1
  68. package/tests/unit/decorators/observed.spec.ts +527 -0
  69. package/tests/unit/modeling/DataDomain.search.spec.ts +188 -0
  70. package/tests/unit/modeling/data_domain_serialization.spec.ts +6 -2
  71. package/tests/unit/modeling/domain_asociation.spec.ts +376 -0
  72. package/tests/unit/modeling/domain_entity.spec.ts +147 -0
  73. package/tests/unit/modeling/domain_property.spec.ts +273 -0
  74. package/build/src/modeling/observed.d.ts.map +0 -1
  75. package/build/src/modeling/observed.js.map +0 -1
@@ -0,0 +1,188 @@
1
+ import { test } from '@japa/runner'
2
+ import { DataDomain } from '../../../src/modeling/DataDomain.js'
3
+ import { DomainEntityKind, DomainNamespaceKind, DomainPropertyKind } from '../../../src/models/kinds.js'
4
+
5
+ test.group('DataDomain Search', () => {
6
+ test('should return all nodes when no search criteria provided', ({ assert }) => {
7
+ const domain = new DataDomain({
8
+ key: 'test-domain',
9
+ info: { name: 'Test Domain' },
10
+ })
11
+
12
+ const namespace = domain.addNamespace({ info: { name: 'TestNamespace' } })
13
+ const model = namespace.addModel({ info: { name: 'TestModel' } })
14
+ const entity = model.addEntity({ info: { name: 'TestEntity' } })
15
+ entity.addProperty({ info: { name: 'testProperty' }, type: 'string' })
16
+
17
+ const results = domain.search()
18
+
19
+ assert.isAtLeast(results.length, 4) // namespace, model, entity, property
20
+ assert.isTrue(results.every((result) => result.matchedFields.length === 0))
21
+ })
22
+
23
+ test('should search by text query in name field', ({ assert }) => {
24
+ const domain = new DataDomain({
25
+ key: 'test-domain',
26
+ info: { name: 'Test Domain' },
27
+ })
28
+
29
+ const namespace = domain.addNamespace({ info: { name: 'UserNamespace' } })
30
+ const model = namespace.addModel({ info: { name: 'UserModel' } })
31
+ const entity = model.addEntity({ info: { name: 'User' } })
32
+ entity.addProperty({ info: { name: 'email' }, type: 'string' })
33
+
34
+ const results = domain.search({ query: 'User' })
35
+
36
+ assert.equal(results.length, 3) // namespace, model, entity
37
+ assert.isTrue(results.every((result) => result.matchedFields.includes('name')))
38
+ })
39
+
40
+ test('should search by text query in displayName field', ({ assert }) => {
41
+ const domain = new DataDomain({
42
+ key: 'test-domain',
43
+ info: { name: 'Test Domain' },
44
+ })
45
+
46
+ domain.addNamespace({ info: { name: 'TestNamespace', displayName: 'User Management' } })
47
+
48
+ const results = domain.search({ query: 'Management' })
49
+
50
+ assert.equal(results.length, 1)
51
+ assert.equal(results[0].node.kind, DomainNamespaceKind)
52
+ assert.isTrue(results[0].matchedFields.includes('displayName'))
53
+ })
54
+
55
+ test('should search by text query in description field', ({ assert }) => {
56
+ const domain = new DataDomain({
57
+ key: 'test-domain',
58
+ info: { name: 'Test Domain' },
59
+ })
60
+
61
+ domain.addNamespace({
62
+ info: { name: 'TestNamespace', description: 'Handles user authentication' },
63
+ })
64
+
65
+ const results = domain.search({ query: 'authentication' })
66
+
67
+ assert.equal(results.length, 1)
68
+ assert.equal(results[0].node.kind, DomainNamespaceKind)
69
+ assert.isTrue(results[0].matchedFields.includes('description'))
70
+ })
71
+
72
+ test('should filter by node type', ({ assert }) => {
73
+ const domain = new DataDomain({
74
+ key: 'test-domain',
75
+ info: { name: 'Test Domain' },
76
+ })
77
+
78
+ const namespace = domain.addNamespace({ info: { name: 'UserNamespace' } })
79
+ const model = namespace.addModel({ info: { name: 'UserModel' } })
80
+ const entity = model.addEntity({ info: { name: 'User' } })
81
+ entity.addProperty({ info: { name: 'userId' }, type: 'string' })
82
+
83
+ const entityResults = domain.search({ nodeTypes: [DomainEntityKind] })
84
+ const propertyResults = domain.search({ nodeTypes: [DomainPropertyKind] })
85
+
86
+ assert.equal(entityResults.length, 1)
87
+ assert.equal(entityResults[0].node.kind, DomainEntityKind)
88
+
89
+ assert.equal(propertyResults.length, 1)
90
+ assert.equal(propertyResults[0].node.kind, DomainPropertyKind)
91
+ })
92
+
93
+ test('should combine text query and node type filters', ({ assert }) => {
94
+ const domain = new DataDomain({
95
+ key: 'test-domain',
96
+ info: { name: 'Test Domain' },
97
+ })
98
+
99
+ const namespace = domain.addNamespace({ info: { name: 'UserNamespace' } })
100
+ const model = namespace.addModel({ info: { name: 'UserModel' } })
101
+ const entity = model.addEntity({ info: { name: 'User' } })
102
+ entity.addProperty({ info: { name: 'userName' }, type: 'string' })
103
+
104
+ const results = domain.search({
105
+ query: 'User',
106
+ nodeTypes: [DomainEntityKind, DomainPropertyKind],
107
+ })
108
+
109
+ assert.equal(results.length, 2) // entity and property
110
+ assert.isTrue(
111
+ results.every((result) => result.node.kind === DomainEntityKind || result.node.kind === DomainPropertyKind)
112
+ )
113
+ })
114
+
115
+ test('should support case-sensitive search', ({ assert }) => {
116
+ const domain = new DataDomain({
117
+ key: 'test-domain',
118
+ info: { name: 'Test Domain' },
119
+ })
120
+
121
+ domain.addNamespace({ info: { name: 'user' } })
122
+ domain.addNamespace({ info: { name: 'User' } })
123
+
124
+ const caseSensitiveResults = domain.search({ query: 'User', caseSensitive: true })
125
+ const caseInsensitiveResults = domain.search({ query: 'User', caseSensitive: false })
126
+
127
+ assert.equal(caseSensitiveResults.length, 1)
128
+ assert.equal(caseInsensitiveResults.length, 2)
129
+ })
130
+
131
+ test('should support regex queries', ({ assert }) => {
132
+ const domain = new DataDomain({
133
+ key: 'test-domain',
134
+ info: { name: 'Test Domain' },
135
+ })
136
+
137
+ domain.addNamespace({ info: { name: 'user1' } })
138
+ domain.addNamespace({ info: { name: 'user2' } })
139
+ domain.addNamespace({ info: { name: 'admin' } })
140
+
141
+ const results = domain.search({ query: /^user\d+$/ })
142
+
143
+ assert.equal(results.length, 2)
144
+ assert.isTrue(results.every((result) => /^user\d+$/.test(result.node.info.name || '')))
145
+ })
146
+
147
+ test('should identify foreign nodes correctly', ({ assert }) => {
148
+ const foreignDomain = new DataDomain({
149
+ key: 'foreign-domain',
150
+ info: { name: 'Foreign Domain', version: '1.0.0' },
151
+ })
152
+ foreignDomain.addNamespace({ info: { name: 'ForeignNamespace' } })
153
+
154
+ const domain = new DataDomain({
155
+ key: 'main-domain',
156
+ info: { name: 'Main Domain' },
157
+ })
158
+ domain.registerForeignDomain(foreignDomain)
159
+
160
+ const resultsWithoutForeign = domain.search({ query: 'Foreign' })
161
+ const resultsWithForeign = domain.search({ query: 'Foreign', includeForeignDomains: true })
162
+
163
+ assert.equal(resultsWithoutForeign.length, 0)
164
+ assert.equal(resultsWithForeign.length, 1)
165
+ assert.isTrue(resultsWithForeign[0].isForeign)
166
+ })
167
+
168
+ test('should handle multiple matched fields', ({ assert }) => {
169
+ const domain = new DataDomain({
170
+ key: 'test-domain',
171
+ info: { name: 'Test Domain' },
172
+ })
173
+
174
+ domain.addNamespace({
175
+ info: {
176
+ name: 'TestNamespace',
177
+ displayName: 'Test Display',
178
+ description: 'Test description',
179
+ },
180
+ })
181
+
182
+ const results = domain.search({ query: 'Test' })
183
+
184
+ assert.equal(results.length, 1)
185
+ assert.equal(results[0].matchedFields.length, 3)
186
+ assert.includeMembers(results[0].matchedFields, ['name', 'displayName', 'description'])
187
+ })
188
+ })
@@ -456,7 +456,7 @@ test.group('Validation Tests', () => {
456
456
  assert.throws(() => domain.toJSON())
457
457
  }).tags(['@modeling', '@serialization', '@validation'])
458
458
 
459
- test('should throw validation error when association has no target entities', ({ assert }) => {
459
+ test('should not throw validation error when association has no target entities', ({ assert }) => {
460
460
  const domain = new DataDomain()
461
461
  const m1 = domain.addModel()
462
462
  const e1 = m1.addEntity()
@@ -466,7 +466,11 @@ test.group('Validation Tests', () => {
466
466
  // Remove the association target edge
467
467
  domain.graph.removeEdge(a1.key, e2.key)
468
468
 
469
- assert.throws(() => domain.toJSON())
469
+ // Note, we specifically removed this from the validation as it would throw errors
470
+ // when associations were created without targets. This is not a structural issue and because
471
+ // of that, it shouldn't be considered a validation error in this context.
472
+ // It is reported through the validator, though.
473
+ assert.doesNotThrow(() => domain.toJSON())
470
474
  }).tags(['@modeling', '@serialization', '@validation'])
471
475
 
472
476
  test('should throw validation error when association references non-existent target', ({ assert }) => {
@@ -908,3 +908,379 @@ test.group('DomainAssociation.hasSemantic()', () => {
908
908
  assert.isFalse(association.hasSemantic(SemanticType.ResourceOwnerIdentifier))
909
909
  })
910
910
  })
911
+
912
+ test.group('DomainAssociation.duplicate()', () => {
913
+ test('duplicates an association successfully', ({ assert }) => {
914
+ const dataDomain = new DataDomain()
915
+ const model = dataDomain.addModel()
916
+ const entity = model.addEntity()
917
+ const targetEntity = model.addEntity()
918
+
919
+ const association = entity.addAssociation({ key: targetEntity.key })
920
+ association.info.name = 'originalAssociation'
921
+ association.required = true
922
+ association.multiple = false
923
+
924
+ const duplicate = association.duplicate()
925
+
926
+ assert.isDefined(duplicate)
927
+ assert.notEqual(duplicate.key, association.key)
928
+ assert.equal(duplicate.info.name, 'originalAssociation_copy')
929
+ assert.equal(duplicate.required, true)
930
+ assert.equal(duplicate.multiple, false)
931
+ })
932
+
933
+ test('places duplicate right after original association in fields list', ({ assert }) => {
934
+ const dataDomain = new DataDomain()
935
+ const model = dataDomain.addModel()
936
+ const entity = model.addEntity()
937
+ const targetEntity = model.addEntity()
938
+
939
+ // Add multiple fields to test ordering
940
+ const property1 = entity.addProperty({ info: { name: 'first' } })
941
+ const association1 = entity.addAssociation({ key: targetEntity.key })
942
+ association1.info.name = 'originalAssoc'
943
+ const property2 = entity.addProperty({ info: { name: 'second' } })
944
+
945
+ const duplicate = association1.duplicate()
946
+
947
+ // Check that duplicate is placed right after association1
948
+ const fieldKeys = entity.fields.map((f) => f.key)
949
+ const association1Index = fieldKeys.indexOf(association1.key)
950
+ const duplicateIndex = fieldKeys.indexOf(duplicate.key)
951
+
952
+ assert.equal(duplicateIndex, association1Index + 1)
953
+ assert.deepEqual(fieldKeys, [property1.key, association1.key, duplicate.key, property2.key])
954
+ })
955
+
956
+ test('generates unique name when base name conflicts', ({ assert }) => {
957
+ const dataDomain = new DataDomain()
958
+ const model = dataDomain.addModel()
959
+ const entity = model.addEntity()
960
+ const targetEntity = model.addEntity()
961
+
962
+ const association = entity.addAssociation({ key: targetEntity.key })
963
+ association.info.name = 'test'
964
+ // Add another association that conflicts with the first generated name
965
+ const conflictAssoc = entity.addAssociation({ key: targetEntity.key })
966
+ conflictAssoc.info.name = 'test_copy'
967
+
968
+ const duplicate = association.duplicate()
969
+
970
+ assert.equal(duplicate.info.name, 'test_copy_2')
971
+ })
972
+
973
+ test('uses fallback name when original association has no name', ({ assert }) => {
974
+ const dataDomain = new DataDomain()
975
+ const model = dataDomain.addModel()
976
+ const entity = model.addEntity()
977
+ const targetEntity = model.addEntity()
978
+
979
+ const association = entity.addAssociation({ key: targetEntity.key })
980
+ // The association gets the default name "new_association" from schema creation
981
+ assert.equal(association.info.name, 'new_association')
982
+
983
+ const duplicate = association.duplicate()
984
+
985
+ assert.equal(duplicate.info.name, 'new_association_copy')
986
+ })
987
+
988
+ test('uses field fallback when association name is explicitly undefined', ({ assert }) => {
989
+ const dataDomain = new DataDomain()
990
+ const model = dataDomain.addModel()
991
+ const entity = model.addEntity()
992
+ const targetEntity = model.addEntity()
993
+
994
+ const association = entity.addAssociation({ key: targetEntity.key })
995
+ association.info.name = 'test'
996
+ // Manually set name to undefined to test the fallback logic
997
+ association.info.name = undefined
998
+
999
+ const duplicate = association.duplicate()
1000
+
1001
+ assert.equal(duplicate.info.name, 'field_copy')
1002
+ })
1003
+
1004
+ test('does not copy key property', ({ assert }) => {
1005
+ const dataDomain = new DataDomain()
1006
+ const model = dataDomain.addModel()
1007
+ const entity = model.addEntity()
1008
+ const targetEntity = model.addEntity()
1009
+
1010
+ const association = entity.addAssociation({ key: targetEntity.key })
1011
+ const originalKey = association.key
1012
+
1013
+ const duplicate = association.duplicate()
1014
+
1015
+ assert.notEqual(duplicate.key, originalKey)
1016
+ })
1017
+
1018
+ test('does not copy targets initially but recreates them', ({ assert }) => {
1019
+ const dataDomain = new DataDomain()
1020
+ const model = dataDomain.addModel()
1021
+ const entity = model.addEntity()
1022
+ const targetEntity1 = model.addEntity()
1023
+ const targetEntity2 = model.addEntity()
1024
+
1025
+ const association = entity.addAssociation({ key: targetEntity1.key })
1026
+ association.addTarget(targetEntity2.key)
1027
+ association.info.name = 'multiTarget'
1028
+
1029
+ const duplicate = association.duplicate()
1030
+
1031
+ // Should have the same targets but different association instance
1032
+ const originalTargets = [...association.listTargets()]
1033
+ const duplicateTargets = [...duplicate.listTargets()]
1034
+
1035
+ assert.equal(originalTargets.length, duplicateTargets.length)
1036
+ assert.equal(originalTargets.length, 2)
1037
+
1038
+ // Check that target keys match
1039
+ const originalKeys = originalTargets.map((t) => t.key).sort()
1040
+ const duplicateKeys = duplicateTargets.map((t) => t.key).sort()
1041
+ assert.deepEqual(originalKeys, duplicateKeys)
1042
+ })
1043
+
1044
+ test('copies all other association attributes', ({ assert }) => {
1045
+ const dataDomain = new DataDomain()
1046
+ const model = dataDomain.addModel()
1047
+ const entity = model.addEntity()
1048
+ const targetEntity = model.addEntity()
1049
+
1050
+ const association = entity.addAssociation({ key: targetEntity.key })
1051
+ association.info.name = 'complexAssociation'
1052
+ association.info.description = 'A complex association'
1053
+ association.required = true
1054
+ association.multiple = true
1055
+ association.readOnly = true
1056
+ association.onDelete = 'cascade'
1057
+ association.semantics = [{ id: SemanticType.ResourceOwnerIdentifier }]
1058
+ association.schema = { linked: true, unionType: 'allOf' }
1059
+ association.bindings = [{ type: 'web', schema: { hidden: true } }]
1060
+
1061
+ const duplicate = association.duplicate()
1062
+
1063
+ assert.equal(duplicate.info.name, 'complexAssociation_copy')
1064
+ assert.equal(duplicate.info.description, 'A complex association')
1065
+ assert.equal(duplicate.required, true)
1066
+ assert.equal(duplicate.multiple, true)
1067
+ assert.equal(duplicate.readOnly, true)
1068
+ assert.equal(duplicate.onDelete, 'cascade')
1069
+ assert.deepEqual(duplicate.semantics, [{ id: SemanticType.ResourceOwnerIdentifier }])
1070
+ assert.deepEqual(duplicate.schema, { linked: true, unionType: 'allOf' })
1071
+ assert.deepEqual(duplicate.bindings, [{ type: 'web', schema: { hidden: true } }])
1072
+ })
1073
+
1074
+ test('creates independent copy of schema and bindings', ({ assert }) => {
1075
+ const dataDomain = new DataDomain()
1076
+ const model = dataDomain.addModel()
1077
+ const entity = model.addEntity()
1078
+ const targetEntity = model.addEntity()
1079
+
1080
+ const association = entity.addAssociation({ key: targetEntity.key })
1081
+ association.info.name = 'original'
1082
+ association.schema = { linked: false, unionType: 'anyOf' }
1083
+ association.bindings = [{ type: 'web', schema: { hidden: false } }]
1084
+
1085
+ const duplicate = association.duplicate()
1086
+
1087
+ // Modify original schema and bindings
1088
+ association.schema!.linked = true
1089
+ association.bindings[0].schema = { hidden: true }
1090
+
1091
+ // Duplicate should be unaffected
1092
+ assert.equal(duplicate.schema!.linked, false)
1093
+ assert.deepEqual(duplicate.bindings[0].schema, { hidden: false })
1094
+ })
1095
+
1096
+ test('copies targets correctly', ({ assert }) => {
1097
+ const dataDomain = new DataDomain()
1098
+ const model = dataDomain.addModel()
1099
+ const entity = model.addEntity()
1100
+
1101
+ const targetEntity1 = model.addEntity()
1102
+ const targetEntity2 = model.addEntity()
1103
+
1104
+ const association = entity.addAssociation()
1105
+ association.addTarget(targetEntity1.key)
1106
+ association.addTarget(targetEntity2.key)
1107
+ association.info.name = 'multiTargetAssoc'
1108
+
1109
+ // Check that original association has targets
1110
+ const originalTargets = [...association.listTargets()]
1111
+ assert.equal(originalTargets.length, 2)
1112
+
1113
+ const duplicate = association.duplicate()
1114
+
1115
+ // Check targets on duplicate
1116
+ const duplicateTargets = [...duplicate.listTargets()]
1117
+ const duplicateTargetObjects = duplicate.targets
1118
+
1119
+ // Check targets array structure
1120
+ assert.equal(duplicateTargetObjects.length, 2)
1121
+ assert.equal(duplicateTargetObjects[0].key, targetEntity1.key)
1122
+ assert.equal(duplicateTargetObjects[1].key, targetEntity2.key)
1123
+
1124
+ // Check resolved targets
1125
+ assert.equal(duplicateTargets.length, 2)
1126
+ const duplicateKeys = duplicateTargets.map((t) => t.key).sort()
1127
+ const expectedKeys = [targetEntity1.key, targetEntity2.key].sort()
1128
+ assert.deepEqual(duplicateKeys, expectedKeys)
1129
+ })
1130
+
1131
+ test('notifies domain of changes', ({ assert }) => {
1132
+ const dataDomain = new DataDomain()
1133
+ const model = dataDomain.addModel()
1134
+ const entity = model.addEntity()
1135
+ const targetEntity = model.addEntity()
1136
+
1137
+ const association = entity.addAssociation({ key: targetEntity.key })
1138
+ association.info.name = 'test'
1139
+
1140
+ let notificationCalled = false
1141
+ const originalNotify = dataDomain.notifyChange
1142
+ dataDomain.notifyChange = () => {
1143
+ notificationCalled = true
1144
+ originalNotify.call(dataDomain)
1145
+ }
1146
+
1147
+ association.duplicate()
1148
+
1149
+ assert.isTrue(notificationCalled)
1150
+
1151
+ // Restore original method
1152
+ dataDomain.notifyChange = originalNotify
1153
+ })
1154
+
1155
+ test('throws error when association has no parent entity', ({ assert }) => {
1156
+ const dataDomain = new DataDomain()
1157
+ const association = new DomainAssociation(dataDomain, 'non-existent-parent', {
1158
+ info: { name: 'orphan' },
1159
+ })
1160
+
1161
+ assert.throws(() => {
1162
+ association.duplicate()
1163
+ }, 'Cannot duplicate association')
1164
+ })
1165
+
1166
+ test('throws error when association is not in parent entity fields list', ({ assert }) => {
1167
+ const dataDomain = new DataDomain()
1168
+ const model = dataDomain.addModel()
1169
+ const entity = model.addEntity()
1170
+ const association = new DomainAssociation(dataDomain, entity.key, {
1171
+ info: { name: 'detached' },
1172
+ })
1173
+
1174
+ // Add association to graph but not to entity fields
1175
+ dataDomain.graph.setNode(association.key, association)
1176
+
1177
+ assert.throws(() => {
1178
+ association.duplicate()
1179
+ }, 'does not exist on the parent entity fields list')
1180
+ })
1181
+
1182
+ test('works with empty entity fields list', ({ assert }) => {
1183
+ const dataDomain = new DataDomain()
1184
+ const model = dataDomain.addModel()
1185
+ const entity = model.addEntity()
1186
+ const targetEntity = model.addEntity()
1187
+ const association = entity.addAssociation({ key: targetEntity.key })
1188
+ association.info.name = 'only'
1189
+
1190
+ const duplicate = association.duplicate()
1191
+
1192
+ assert.equal(entity.fields.length, 2)
1193
+ assert.equal(entity.fields[0].key, association.key)
1194
+ assert.equal(entity.fields[1].key, duplicate.key)
1195
+ })
1196
+
1197
+ test('handles duplication with mixed field types', ({ assert }) => {
1198
+ const dataDomain = new DataDomain()
1199
+ const model = dataDomain.addModel()
1200
+ const entity = model.addEntity()
1201
+ const targetEntity = model.addEntity()
1202
+
1203
+ const property1 = entity.addProperty({ info: { name: 'prop1' } })
1204
+ const association1 = entity.addAssociation({ key: targetEntity.key })
1205
+ association1.info.name = 'assoc1'
1206
+ const property2 = entity.addProperty({ info: { name: 'prop2' } })
1207
+
1208
+ const duplicate = association1.duplicate()
1209
+
1210
+ // Should maintain correct order
1211
+ const fieldKeys = entity.fields.map((f) => f.key)
1212
+ assert.deepEqual(fieldKeys, [property1.key, association1.key, duplicate.key, property2.key])
1213
+ })
1214
+
1215
+ test('preserves field types correctly', ({ assert }) => {
1216
+ const dataDomain = new DataDomain()
1217
+ const model = dataDomain.addModel()
1218
+ const entity = model.addEntity()
1219
+ const targetEntity = model.addEntity()
1220
+ const association = entity.addAssociation({ key: targetEntity.key })
1221
+
1222
+ const duplicate = association.duplicate()
1223
+
1224
+ // Check that both original and duplicate have correct field type
1225
+ const originalField = entity.fields.find((f) => f.key === association.key)
1226
+ const duplicateField = entity.fields.find((f) => f.key === duplicate.key)
1227
+
1228
+ assert.equal(originalField?.type, 'association')
1229
+ assert.equal(duplicateField?.type, 'association')
1230
+ })
1231
+
1232
+ test('handles multiple target entities correctly', ({ assert }) => {
1233
+ const dataDomain = new DataDomain()
1234
+ const model = dataDomain.addModel()
1235
+ const entity = model.addEntity()
1236
+ const targetEntity1 = model.addEntity()
1237
+ const targetEntity2 = model.addEntity()
1238
+ const targetEntity3 = model.addEntity()
1239
+
1240
+ const association = entity.addAssociation()
1241
+ association.addTarget(targetEntity1.key)
1242
+ association.addTarget(targetEntity2.key)
1243
+ association.addTarget(targetEntity3.key)
1244
+ association.info.name = 'multiTarget'
1245
+
1246
+ const duplicate = association.duplicate()
1247
+
1248
+ const originalTargets = [...association.listTargets()]
1249
+ const duplicateTargets = [...duplicate.listTargets()]
1250
+
1251
+ assert.equal(originalTargets.length, 3)
1252
+ assert.equal(duplicateTargets.length, 3)
1253
+
1254
+ // Verify all targets are copied correctly
1255
+ const originalKeys = originalTargets.map((t) => t.key).sort()
1256
+ const duplicateKeys = duplicateTargets.map((t) => t.key).sort()
1257
+ assert.deepEqual(originalKeys, duplicateKeys)
1258
+ })
1259
+
1260
+ test('handles foreign domains correctly', ({ assert }) => {
1261
+ const fd = new DataDomain()
1262
+ const fd1 = fd.addModel()
1263
+ const fe1 = fd1.addEntity()
1264
+ fd.info.version = '1.0.0'
1265
+
1266
+ const dataDomain = new DataDomain()
1267
+ const model = dataDomain.addModel()
1268
+ const entity = model.addEntity()
1269
+ dataDomain.registerForeignDomain(fd)
1270
+
1271
+ const association = entity.addAssociation({ key: fe1.key, domain: fd.key })
1272
+
1273
+ const duplicate = association.duplicate()
1274
+
1275
+ const originalTargets = [...association.listTargets()]
1276
+ const duplicateTargets = [...duplicate.listTargets()]
1277
+
1278
+ assert.equal(originalTargets.length, 1)
1279
+ assert.equal(duplicateTargets.length, 1)
1280
+
1281
+ // Verify all targets are copied correctly
1282
+ const originalKeys = originalTargets.map((t) => t.key).sort()
1283
+ const duplicateKeys = duplicateTargets.map((t) => t.key).sort()
1284
+ assert.deepEqual(originalKeys, duplicateKeys)
1285
+ })
1286
+ })