@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
@@ -42068,10 +42068,10 @@
42068
42068
  "@id": "#194"
42069
42069
  },
42070
42070
  {
42071
- "@id": "#200"
42071
+ "@id": "#197"
42072
42072
  },
42073
42073
  {
42074
- "@id": "#197"
42074
+ "@id": "#200"
42075
42075
  },
42076
42076
  {
42077
42077
  "@id": "#203"
@@ -43478,7 +43478,7 @@
43478
43478
  "doc:ExternalDomainElement",
43479
43479
  "doc:DomainElement"
43480
43480
  ],
43481
- "doc:raw": "class: '3'\ndescription: '150 - 300'\nnumberOfFte: 5500\nnumberOfEmployees: 5232\n",
43481
+ "doc:raw": "code: '5'\ndescription: 'Limited company'\n",
43482
43482
  "core:mediaType": "application/yaml",
43483
43483
  "sourcemaps:sources": [
43484
43484
  {
@@ -43499,7 +43499,7 @@
43499
43499
  "doc:ExternalDomainElement",
43500
43500
  "doc:DomainElement"
43501
43501
  ],
43502
- "doc:raw": "code: '5'\ndescription: 'Limited company'\n",
43502
+ "doc:raw": "class: '3'\ndescription: '150 - 300'\nnumberOfFte: 5500\nnumberOfEmployees: 5232\n",
43503
43503
  "core:mediaType": "application/yaml",
43504
43504
  "sourcemaps:sources": [
43505
43505
  {
@@ -44766,12 +44766,12 @@
44766
44766
  {
44767
44767
  "@id": "#199/source-map/lexical/element_0",
44768
44768
  "sourcemaps:element": "amf://id#199",
44769
- "sourcemaps:value": "[(1,0)-(5,0)]"
44769
+ "sourcemaps:value": "[(1,0)-(3,0)]"
44770
44770
  },
44771
44771
  {
44772
44772
  "@id": "#202/source-map/lexical/element_0",
44773
44773
  "sourcemaps:element": "amf://id#202",
44774
- "sourcemaps:value": "[(1,0)-(3,0)]"
44774
+ "sourcemaps:value": "[(1,0)-(5,0)]"
44775
44775
  },
44776
44776
  {
44777
44777
  "@id": "#205/source-map/lexical/element_0",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@api-client/core",
3
3
  "description": "The API Client's core client library. Works in NodeJS and in a ES enabled browser.",
4
- "version": "0.18.17",
4
+ "version": "0.18.19",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -57,6 +57,7 @@
57
57
  "#authorization/*": "./src/authorization/*",
58
58
  "#cookies/*": "./src/cookies/*",
59
59
  "#data/*": "./src/data/*",
60
+ "#decorators/*": "./src/decorators/*",
60
61
  "#events/*": "./src/events/*",
61
62
  "#lib/*": "./src/lib/*",
62
63
  "#mocking/*": "./src/mocking/*",
@@ -73,10 +73,10 @@ export function toRaw<T extends object = object>(source: object, target: T): T |
73
73
  * - As a class property decorator
74
74
  * - As a class setter decorator
75
75
  *
76
- * The property class either has to have a `root` property
76
+ * The property class either has to have the `domain` property
77
77
  * or a `notifyChange` method. The decorator will call the
78
78
  * `notifyChange` method if it exists. Otherwise, it will
79
- * call the `notifyChange` method of the root domain.
79
+ * call the `notifyChange` method of the root domain (the `domain` property).
80
80
  */
81
81
  export function observed(config: ObserveConfig = {}): PropertyDecorator {
82
82
  return <C extends DomainInstance, V>(
@@ -138,7 +138,7 @@ export function observed(config: ObserveConfig = {}): PropertyDecorator {
138
138
  map = {}
139
139
  Reflect.set(this, reactiveSymbol, map)
140
140
  }
141
- if (map[context.name] === context.name) {
141
+ if (map[context.name] === value) {
142
142
  return
143
143
  }
144
144
  const notify = () => {
@@ -196,7 +196,7 @@ export function observed(config: ObserveConfig = {}): PropertyDecorator {
196
196
  * property itself.
197
197
  */
198
198
  export function retargetChange() {
199
- return <C extends DomainInstance, V extends EventTarget>(
199
+ return <C extends DomainInstance, V extends EventTarget | undefined>(
200
200
  target: StandardPropertyTarget<C, V>,
201
201
  context: StandardPropertyContext<C, V>
202
202
  ): any => {
@@ -215,7 +215,7 @@ export function retargetChange() {
215
215
  map = {}
216
216
  Reflect.set(this, reactiveSymbol, map)
217
217
  }
218
- if (map[context.name] === context.name) {
218
+ if (map[context.name] === value) {
219
219
  return
220
220
  }
221
221
  const oldValue = map[context.name]
@@ -14,7 +14,7 @@ import type {
14
14
  } from './types.js'
15
15
  import { DataDomain } from './DataDomain.js'
16
16
  import { DependentModel, type DependentModelSchema, type DomainDependency } from './DependentModel.js'
17
- import { observed, toRaw } from './observed.js'
17
+ import { observed, toRaw } from '../decorators/observed.js'
18
18
 
19
19
  /**
20
20
  * Contact information for the exposed API.
@@ -15,6 +15,9 @@ import type {
15
15
  DomainGraphEdge,
16
16
  DomainGraphNodeType,
17
17
  SerializedGraph,
18
+ DomainSearchCriteria,
19
+ DomainSearchResult,
20
+ SearchableNodeType,
18
21
  } from './types.js'
19
22
  import { type DomainNamespaceSchema, DomainNamespace, type NamespaceOrderedItem } from './DomainNamespace.js'
20
23
  import { type DomainModelSchema, DomainModel } from './DomainModel.js'
@@ -1361,4 +1364,145 @@ export class DataDomain extends DependentModel {
1361
1364
  throw new Error(`Unknown kind ${kind} for the object ${key}`)
1362
1365
  }
1363
1366
  }
1367
+
1368
+ /**
1369
+ * Searches domain nodes by text content and/or node type.
1370
+ *
1371
+ * This function traverses the graph and filters nodes based on the provided search criteria.
1372
+ * It can search through name, displayName, and description fields of domain objects,
1373
+ * and optionally filter by specific node types.
1374
+ *
1375
+ * @param criteria Search criteria including query string/regex and filtering options.
1376
+ * @returns An array of search results containing matching nodes and metadata.
1377
+ *
1378
+ * @example
1379
+ * ```typescript
1380
+ * // Search for nodes containing "user" in any text field
1381
+ * const results = dataDomain.search({ query: 'user' });
1382
+ *
1383
+ * // Search for entities only
1384
+ * const entityResults = dataDomain.search({
1385
+ * nodeTypes: [DomainEntityKind]
1386
+ * });
1387
+ *
1388
+ * // Search for "user" in entities and properties with case-sensitive matching
1389
+ * const specificResults = dataDomain.search({
1390
+ * query: 'User',
1391
+ * nodeTypes: [DomainEntityKind, DomainPropertyKind],
1392
+ * caseSensitive: true
1393
+ * });
1394
+ *
1395
+ * // Search using regex pattern
1396
+ * const regexResults = dataDomain.search({
1397
+ * query: /^user/i
1398
+ * });
1399
+ * ```
1400
+ */
1401
+ search(criteria: DomainSearchCriteria = {}): DomainSearchResult[] {
1402
+ const { query, nodeTypes, includeForeignDomains = false, caseSensitive = false } = criteria
1403
+
1404
+ const results: DomainSearchResult[] = []
1405
+
1406
+ // If no criteria provided, return all nodes
1407
+ const shouldReturnAll = !query && (!nodeTypes || nodeTypes.length === 0)
1408
+
1409
+ // Helper function to check if a node matches the type filter
1410
+ const matchesNodeType = (nodeKind: string): boolean => {
1411
+ if (!nodeTypes || nodeTypes.length === 0) {
1412
+ return true
1413
+ }
1414
+ return nodeTypes.includes(nodeKind as SearchableNodeType)
1415
+ }
1416
+
1417
+ // Helper function to check if text matches the query
1418
+ const matchesQuery = (text: string | undefined): boolean => {
1419
+ if (!text || !query) {
1420
+ return !query // If no query, consider it a match
1421
+ }
1422
+
1423
+ if (query instanceof RegExp) {
1424
+ return query.test(text)
1425
+ }
1426
+
1427
+ // String query
1428
+ const searchText = caseSensitive ? text : text.toLowerCase()
1429
+ const searchQuery = caseSensitive ? query : query.toLowerCase()
1430
+ return searchText.includes(searchQuery)
1431
+ }
1432
+
1433
+ // Helper function to get matched fields for a node
1434
+ const getMatchedFields = (node: DomainGraphNodeType): ('name' | 'displayName' | 'description')[] => {
1435
+ if (!query) {
1436
+ return [] // No query means no specific field matching
1437
+ }
1438
+
1439
+ const matched: ('name' | 'displayName' | 'description')[] = []
1440
+ const { info } = node
1441
+
1442
+ if (matchesQuery(info.name)) {
1443
+ matched.push('name')
1444
+ }
1445
+ if (matchesQuery(info.displayName)) {
1446
+ matched.push('displayName')
1447
+ }
1448
+ if (matchesQuery(info.description)) {
1449
+ matched.push('description')
1450
+ }
1451
+
1452
+ return matched
1453
+ }
1454
+
1455
+ // Helper function to determine if a node key belongs to a foreign domain
1456
+ const isForeignNode = (nodeKey: string): boolean => {
1457
+ return nodeKey.includes(':')
1458
+ }
1459
+
1460
+ // Iterate through all nodes in the graph
1461
+ for (const nodeKey of this.graph.nodes()) {
1462
+ const node = this.graph.node(nodeKey) as DomainGraphNodeType
1463
+
1464
+ // Skip if node doesn't exist or doesn't have required properties
1465
+ if (!node || !node.info) {
1466
+ continue
1467
+ }
1468
+
1469
+ const isForeign = isForeignNode(nodeKey)
1470
+
1471
+ // Skip foreign nodes if not requested
1472
+ if (isForeign && !includeForeignDomains) {
1473
+ continue
1474
+ }
1475
+
1476
+ // Check node type filter
1477
+ if (!matchesNodeType(node.kind)) {
1478
+ continue
1479
+ }
1480
+
1481
+ // If returning all nodes (no specific criteria), add the node
1482
+ if (shouldReturnAll) {
1483
+ results.push({
1484
+ node,
1485
+ matchedFields: [],
1486
+ key: nodeKey,
1487
+ isForeign,
1488
+ })
1489
+ continue
1490
+ }
1491
+
1492
+ // Check text query match
1493
+ const matchedFields = getMatchedFields(node)
1494
+ const hasTextMatch = !query || matchedFields.length > 0
1495
+
1496
+ if (hasTextMatch) {
1497
+ results.push({
1498
+ node,
1499
+ matchedFields,
1500
+ key: nodeKey,
1501
+ isForeign,
1502
+ })
1503
+ }
1504
+ }
1505
+
1506
+ return results
1507
+ }
1364
1508
  }
@@ -3,7 +3,7 @@ import { DomainAssociationKind } from '../models/kinds.js'
3
3
  import { DomainElement, type DomainElementSchema } from './DomainElement.js'
4
4
  import { nanoid } from '../nanoid.js'
5
5
  import { type IThing, Thing } from '../models/Thing.js'
6
- import { observed, retargetChange, toRaw } from './observed.js'
6
+ import { observed, retargetChange, toRaw } from '../decorators/observed.js'
7
7
  import type { DomainEntity } from './DomainEntity.js'
8
8
  import type { IApiAssociationShape, IApiPropertyShape } from '../amf/definitions/Shapes.js'
9
9
  import type { AssociationBinding, AssociationBindings, AssociationWebBindings } from './Bindings.js'
@@ -591,4 +591,54 @@ export class DomainAssociation extends DomainElement {
591
591
  hasSemantic(semanticId: SemanticType): boolean {
592
592
  return this.semantics.some((s) => s.id === semanticId)
593
593
  }
594
+
595
+ /**
596
+ * Creates a duplicate of this association.
597
+ * It places the duplicate on the parent entity right after the original association.
598
+ *
599
+ * @returns A new `DomainAssociation` instance that is a duplicate of this one.
600
+ */
601
+ duplicate(): DomainAssociation {
602
+ const parent = this.getParentInstance()
603
+ if (!parent) {
604
+ throw new Error(`Cannot duplicate association ${this.key} as it has no parent entity.`)
605
+ }
606
+ const originalIndex = parent.fields.findIndex((f) => f.key === this.key)
607
+ if (originalIndex === -1) {
608
+ throw new Error(`Cannot duplicate association ${this.key} as it does not exist on the parent entity fields list.`)
609
+ }
610
+ const baseName = this.info.name || 'field'
611
+ const newName = parent.generateUniqueName(baseName)
612
+
613
+ // Making a copy and restoring it through the `addAssociation()` method
614
+ // scales better than copying it manually.
615
+ const copy = this.toJSON() as Partial<DomainAssociationSchema>
616
+
617
+ // Delete properties that should not be copied
618
+ delete copy.key
619
+ delete copy.targets
620
+
621
+ // Set the new name for the duplicated association
622
+ if (copy.info) {
623
+ copy.info.name = newName
624
+ } else {
625
+ copy.info = { name: newName }
626
+ }
627
+
628
+ const result = parent.addAssociation({}, copy)
629
+
630
+ // Copy the target entities
631
+ for (const target of this.targets) {
632
+ result.addTarget(target.key, target.domain)
633
+ }
634
+
635
+ // Move the duplicate to be right after the original
636
+ const fromIndex = parent.fields.length - 1
637
+ const duplicateField = parent.fields[fromIndex]
638
+ parent.fields.splice(fromIndex, 1)
639
+ parent.fields.splice(originalIndex + 1, 0, duplicateField)
640
+
641
+ this.domain.notifyChange()
642
+ return result
643
+ }
594
644
  }
@@ -3,7 +3,7 @@ import { DomainAssociationKind, DomainEntityKind, DomainPropertyKind } from '../
3
3
  import { DomainElement, type DomainElementSchema } from './DomainElement.js'
4
4
  import { nanoid } from '../nanoid.js'
5
5
  import { type IThing, Thing } from '../models/Thing.js'
6
- import { observed, retargetChange, toRaw } from './observed.js'
6
+ import { observed, retargetChange, toRaw } from '../decorators/observed.js'
7
7
  import type { IShapeUnion } from '../amf/definitions/Shapes.js'
8
8
  import { DomainProperty, type DomainPropertySchema } from './DomainProperty.js'
9
9
  import { RemovePropertyException } from '../exceptions/remove_property_exception.js'
@@ -880,4 +880,27 @@ export class DomainEntity extends DomainElement {
880
880
  hasSemantic(semanticId: SemanticType): boolean {
881
881
  return this.semantics.some((s) => s.id === semanticId)
882
882
  }
883
+
884
+ /**
885
+ * Generates a unique name by appending a number to the base name.
886
+ * @param baseName The base name to make unique.
887
+ * @returns A unique name within the current entity.
888
+ */
889
+ generateUniqueName(baseName: string): string {
890
+ const existingNames = new Set<string>()
891
+ for (const field of this.listFields()) {
892
+ if (field.info.name) {
893
+ existingNames.add(field.info.name)
894
+ }
895
+ }
896
+
897
+ let counter = 2
898
+ let newName = `${baseName}_copy`
899
+ while (existingNames.has(newName)) {
900
+ newName = `${baseName}_copy_${counter}`
901
+ counter++
902
+ }
903
+
904
+ return newName
905
+ }
883
906
  }
@@ -3,7 +3,7 @@ import { DomainEntityKind, DomainModelKind } from '../models/kinds.js'
3
3
  import { DomainElement, DomainElementSchema } from './DomainElement.js'
4
4
  import { nanoid } from '../nanoid.js'
5
5
  import { type IThing, Thing } from '../models/Thing.js'
6
- import { observed, retargetChange } from './observed.js'
6
+ import { observed, retargetChange } from '../decorators/observed.js'
7
7
  import type { DomainNamespace } from './DomainNamespace.js'
8
8
  import type { FileBreadcrumb } from '../models/store/File.js'
9
9
  import { DomainEntity, type DomainEntitySchema } from './DomainEntity.js'
@@ -3,7 +3,7 @@ import { DomainModelKind, DomainNamespaceKind } from '../models/kinds.js'
3
3
  import { DomainElement, DomainElementSchema } from './DomainElement.js'
4
4
  import { nanoid } from '../nanoid.js'
5
5
  import { IThing, Thing } from '../models/Thing.js'
6
- import { observed, retargetChange } from './observed.js'
6
+ import { observed, retargetChange } from '../decorators/observed.js'
7
7
  import { DomainModel, DomainModelSchema } from './DomainModel.js'
8
8
  import { RemoveNamespaceException } from '../exceptions/remove_namespace_exception.js'
9
9
  import { RemoveModelException } from '../exceptions/remove_model_exception.js'
@@ -3,7 +3,7 @@ import { DomainPropertyKind } from '../models/kinds.js'
3
3
  import { DomainElement, type DomainElementSchema } from './DomainElement.js'
4
4
  import { nanoid } from '../nanoid.js'
5
5
  import { type IThing, Thing } from '../models/Thing.js'
6
- import { observed, retargetChange, toRaw } from './observed.js'
6
+ import { observed, retargetChange, toRaw } from '../decorators/observed.js'
7
7
  import {
8
8
  type BinaryFormat,
9
9
  BinaryFormats,
@@ -44,6 +44,11 @@ export interface DomainPropertySchema extends DomainElementSchema {
44
44
  * Whether this property describes a primary key of the entity.
45
45
  */
46
46
  primary?: boolean
47
+ /**
48
+ * Whether this property describes a unique property of the entity.
49
+ * This is used to generate unique constraints in the database.
50
+ */
51
+ unique?: boolean
47
52
  /**
48
53
  * Whether this property describes an indexed property of the entity.
49
54
  */
@@ -158,6 +163,12 @@ export class DomainProperty extends DomainElement {
158
163
  */
159
164
  @observed() accessor primary: boolean | undefined
160
165
 
166
+ /**
167
+ * Whether this property describes a unique property of the entity.
168
+ * This is used to generate unique constraints in the database.
169
+ */
170
+ @observed() accessor unique: boolean | undefined
171
+
161
172
  /**
162
173
  * Whether this property describes an indexed property of the entity.
163
174
  */
@@ -230,6 +241,7 @@ export class DomainProperty extends DomainElement {
230
241
  type = DomainPropertyList.string,
231
242
  index,
232
243
  primary,
244
+ unique,
233
245
  readOnly,
234
246
  writeOnly,
235
247
  tags,
@@ -262,6 +274,9 @@ export class DomainProperty extends DomainElement {
262
274
  if (typeof primary === 'boolean') {
263
275
  result.primary = primary
264
276
  }
277
+ if (typeof unique === 'boolean') {
278
+ result.unique = unique
279
+ }
265
280
  if (typeof readOnly === 'boolean') {
266
281
  result.readOnly = readOnly
267
282
  }
@@ -339,6 +354,11 @@ export class DomainProperty extends DomainElement {
339
354
  } else {
340
355
  this.primary = undefined
341
356
  }
357
+ if (typeof init.unique === 'boolean') {
358
+ this.unique = init.unique
359
+ } else {
360
+ this.unique = undefined
361
+ }
342
362
  if (typeof init.readOnly === 'boolean') {
343
363
  this.readOnly = init.readOnly
344
364
  } else {
@@ -405,6 +425,9 @@ export class DomainProperty extends DomainElement {
405
425
  if (typeof this.primary === 'boolean') {
406
426
  result.primary = this.primary
407
427
  }
428
+ if (typeof this.unique === 'boolean') {
429
+ result.unique = this.unique
430
+ }
408
431
  if (typeof this.multiple === 'boolean') {
409
432
  result.multiple = this.multiple
410
433
  }
@@ -617,4 +640,46 @@ export class DomainProperty extends DomainElement {
617
640
  hasSemantic(semanticId: SemanticType): boolean {
618
641
  return this.semantics.some((s) => s.id === semanticId)
619
642
  }
643
+
644
+ /**
645
+ * Creates a duplicate of this domain property on the parent entity.
646
+ * It places the duplicate on the parent entity right after the original property.
647
+ */
648
+ duplicate(): DomainProperty {
649
+ const parent = this.getParentInstance()
650
+ if (!parent) {
651
+ throw new Error(`Cannot duplicate property ${this.key} as it has no parent entity.`)
652
+ }
653
+ const originalIndex = parent.fields.findIndex((f) => f.key === this.key)
654
+ if (originalIndex === -1) {
655
+ throw new Error(`Cannot duplicate property ${this.key} as it does not exist on the parent entity fields list.`)
656
+ }
657
+ const baseName = this.info.name || 'field'
658
+ const newName = parent.generateUniqueName(baseName)
659
+
660
+ // Making a copy and restoring it through the `addProperty()` method
661
+ // scales better than copying it manually.
662
+ const copy = this.toJSON() as Partial<DomainPropertySchema>
663
+
664
+ // Delete properties that should not be copied
665
+ delete copy.key
666
+ delete copy.primary // Don't duplicate the primary key
667
+
668
+ // Set the new name for the duplicated property
669
+ if (copy.info) {
670
+ copy.info.name = newName
671
+ } else {
672
+ copy.info = { name: newName }
673
+ }
674
+ const result = parent.addProperty(copy)
675
+
676
+ // Move the duplicate to be right after the original
677
+ const fromIndex = parent.fields.length - 1
678
+ const duplicateField = parent.fields[fromIndex]
679
+ parent.fields.splice(fromIndex, 1)
680
+ parent.fields.splice(originalIndex + 1, 0, duplicateField)
681
+
682
+ this.domain.notifyChange()
683
+ return result
684
+ }
620
685
  }
@@ -200,10 +200,8 @@ function validateGraphConsistency(g: DataDomainGraph, domainKey: string): void {
200
200
  })
201
201
 
202
202
  if (targetEdges.length === 0) {
203
- validationErrors.push(
204
- `Association "${association.info.name}" (${nodeId}) has no target entities. ` +
205
- `Associations must reference at least one target entity.`
206
- )
203
+ // This is general warning message, do not an error.
204
+ // We can serialize and deserialize associations without targets.
207
205
  }
208
206
 
209
207
  // Validate that all target entities exist
@@ -152,6 +152,8 @@ export const SKU_PRESETS = {
152
152
  caseMode: 'uppercase',
153
153
  prefix: 'PROD-',
154
154
  enforceUniqueness: true,
155
+ validateReservedWords: true,
156
+ reservedValues: ['ADMIN', 'TEST', 'NULL', 'DEFAULT'],
155
157
  }),
156
158
 
157
159
  /**