@api-client/core 0.16.1 → 0.17.1

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 (29) hide show
  1. package/bin/test.ts +0 -4
  2. package/build/src/index.d.ts +72 -72
  3. package/build/src/index.d.ts.map +1 -1
  4. package/build/src/index.js +3 -3
  5. package/build/src/index.js.map +1 -1
  6. package/build/src/modeling/GraphUtils.d.ts.map +1 -1
  7. package/build/src/modeling/GraphUtils.js +16 -4
  8. package/build/src/modeling/GraphUtils.js.map +1 -1
  9. package/build/src/modeling/Semantics.d.ts +52 -0
  10. package/build/src/modeling/Semantics.d.ts.map +1 -1
  11. package/build/src/modeling/Semantics.js +206 -97
  12. package/build/src/modeling/Semantics.js.map +1 -1
  13. package/build/tsconfig.tsbuildinfo +1 -1
  14. package/data/models/example-generator-api.json +12 -12
  15. package/package.json +1 -1
  16. package/src/modeling/GraphUtils.ts +15 -4
  17. package/src/modeling/Semantics.ts +226 -101
  18. package/tests/unit/modeling/api_model.spec.ts +25 -6
  19. package/tests/unit/modeling/data_domain.spec.ts +47 -47
  20. package/tests/unit/modeling/data_domain_associations.spec.ts +41 -30
  21. package/tests/unit/modeling/data_domain_change_observers.spec.ts +30 -6
  22. package/tests/unit/modeling/data_domain_entities.spec.ts +62 -62
  23. package/tests/unit/modeling/data_domain_foreign.spec.ts +59 -59
  24. package/tests/unit/modeling/data_domain_models.spec.ts +80 -82
  25. package/tests/unit/modeling/data_domain_namespaces.spec.ts +76 -76
  26. package/tests/unit/modeling/data_domain_property.spec.ts +29 -29
  27. package/tests/unit/modeling/data_domain_serialization.spec.ts +9 -9
  28. package/tests/unit/modeling/domain_asociation_targets.spec.ts +26 -26
  29. package/tests/unit/modeling/semantics.spec.ts +73 -1
@@ -42062,10 +42062,10 @@
42062
42062
  "@id": "#209"
42063
42063
  },
42064
42064
  {
42065
- "@id": "#191"
42065
+ "@id": "#194"
42066
42066
  },
42067
42067
  {
42068
- "@id": "#194"
42068
+ "@id": "#191"
42069
42069
  },
42070
42070
  {
42071
42071
  "@id": "#197"
@@ -42816,10 +42816,10 @@
42816
42816
  "@id": "#213"
42817
42817
  },
42818
42818
  {
42819
- "@id": "#216"
42819
+ "@id": "#219"
42820
42820
  },
42821
42821
  {
42822
- "@id": "#219"
42822
+ "@id": "#216"
42823
42823
  }
42824
42824
  ],
42825
42825
  "doc:root": false,
@@ -43436,7 +43436,7 @@
43436
43436
  "doc:ExternalDomainElement",
43437
43437
  "doc:DomainElement"
43438
43438
  ],
43439
- "doc:raw": "countryCode: \"BE\"\ngraydonEnterpriseId: 1057155523\nregistrationId: \"0422319093\"\nvatNumber: \"BE0422319093\"\ngraydonCompanyId: \"0422319093\"\nisBranchOffice: false\n",
43439
+ "doc:raw": "addressType: 'REGISTERED-OFFICE-ADDRESS'\nstreetName: 'UITBREIDINGSTRAAT'\nhouseNumber: '84'\nhouseNumberAddition: '/1'\npostalCode: '2600'\ncity: 'BERCHEM (ANTWERPEN)'\ncountry: 'Belgium'\ncountryCode: 'BE'\nfullFormatedAddress: \"UITBREIDINGSTRAAT 84 /1, 2600 BERCHEM (ANTWERPEN), BELIUM\"\n",
43440
43440
  "core:mediaType": "application/yaml",
43441
43441
  "sourcemaps:sources": [
43442
43442
  {
@@ -43457,7 +43457,7 @@
43457
43457
  "doc:ExternalDomainElement",
43458
43458
  "doc:DomainElement"
43459
43459
  ],
43460
- "doc:raw": "addressType: 'REGISTERED-OFFICE-ADDRESS'\nstreetName: 'UITBREIDINGSTRAAT'\nhouseNumber: '84'\nhouseNumberAddition: '/1'\npostalCode: '2600'\ncity: 'BERCHEM (ANTWERPEN)'\ncountry: 'Belgium'\ncountryCode: 'BE'\nfullFormatedAddress: \"UITBREIDINGSTRAAT 84 /1, 2600 BERCHEM (ANTWERPEN), BELIUM\"\n",
43460
+ "doc:raw": "countryCode: \"BE\"\ngraydonEnterpriseId: 1057155523\nregistrationId: \"0422319093\"\nvatNumber: \"BE0422319093\"\ngraydonCompanyId: \"0422319093\"\nisBranchOffice: false\n",
43461
43461
  "core:mediaType": "application/yaml",
43462
43462
  "sourcemaps:sources": [
43463
43463
  {
@@ -44274,7 +44274,7 @@
44274
44274
  "doc:ExternalDomainElement",
44275
44275
  "doc:DomainElement"
44276
44276
  ],
44277
- "doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
44277
+ "doc:raw": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
44278
44278
  "core:mediaType": "application/yaml",
44279
44279
  "sourcemaps:sources": [
44280
44280
  {
@@ -44295,7 +44295,7 @@
44295
44295
  "doc:ExternalDomainElement",
44296
44296
  "doc:DomainElement"
44297
44297
  ],
44298
- "doc:raw": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
44298
+ "doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
44299
44299
  "core:mediaType": "application/yaml",
44300
44300
  "sourcemaps:sources": [
44301
44301
  {
@@ -44756,12 +44756,12 @@
44756
44756
  {
44757
44757
  "@id": "#193/source-map/lexical/element_0",
44758
44758
  "sourcemaps:element": "amf://id#193",
44759
- "sourcemaps:value": "[(1,0)-(7,0)]"
44759
+ "sourcemaps:value": "[(1,0)-(10,0)]"
44760
44760
  },
44761
44761
  {
44762
44762
  "@id": "#196/source-map/lexical/element_0",
44763
44763
  "sourcemaps:element": "amf://id#196",
44764
- "sourcemaps:value": "[(1,0)-(10,0)]"
44764
+ "sourcemaps:value": "[(1,0)-(7,0)]"
44765
44765
  },
44766
44766
  {
44767
44767
  "@id": "#199/source-map/lexical/element_0",
@@ -45126,12 +45126,12 @@
45126
45126
  {
45127
45127
  "@id": "#218/source-map/lexical/element_0",
45128
45128
  "sourcemaps:element": "amf://id#218",
45129
- "sourcemaps:value": "[(1,0)-(7,0)]"
45129
+ "sourcemaps:value": "[(1,0)-(3,0)]"
45130
45130
  },
45131
45131
  {
45132
45132
  "@id": "#221/source-map/lexical/element_0",
45133
45133
  "sourcemaps:element": "amf://id#221",
45134
- "sourcemaps:value": "[(1,0)-(3,0)]"
45134
+ "sourcemaps:value": "[(1,0)-(7,0)]"
45135
45135
  },
45136
45136
  {
45137
45137
  "@id": "#338/source-map/synthesized-field/element_1",
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.16.1",
4
+ "version": "0.17.1",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -1,5 +1,6 @@
1
1
  import type { Graph } from '@api-client/graph/graph/Graph.js'
2
2
  import type { DomainGraphEdge, DomainGraphNodeType } from './types.js'
3
+ import { DomainAssociationKind } from '../models/kinds.js'
3
4
 
4
5
  /**
5
6
  * Recursively removes a node and all its children from the graph.
@@ -9,18 +10,28 @@ export function removeGraphNode(graph: Graph<unknown, DomainGraphNodeType, Domai
9
10
  if (!graph.hasNode(key)) {
10
11
  return
11
12
  }
13
+ const node = graph.node(key)
14
+ if (!node) {
15
+ return
16
+ }
17
+ if (node.kind === DomainAssociationKind) {
18
+ // If the node is an association, remove the association from the graph,
19
+ // but do not follow its children, as it would remove entities associated with it.
20
+ graph.removeNode(key)
21
+ return
22
+ }
12
23
  for (const child of graph.children(key)) {
13
24
  removeGraphNode(graph, child)
14
25
  }
26
+ // Remove all edges from the node.
15
27
  for (const edge of graph.outEdges(key)) {
16
28
  const label = graph.edge(edge)
17
29
  if (!label) {
18
30
  continue
19
31
  }
20
- // While associations should be associated with the entity through an edge,
21
- // I am still on a fence about properties being the same...
22
- // We won't be reusing properties within a domain model.
23
- if (['property', 'association'].includes(label.type)) {
32
+ if (label.type === 'association') {
33
+ removeGraphNode(graph, edge.w)
34
+ } else if (label.type === 'property') {
24
35
  removeGraphNode(graph, edge.w)
25
36
  }
26
37
  }
@@ -227,6 +227,44 @@ export enum SemanticScope {
227
227
  Association = 'Association',
228
228
  }
229
229
 
230
+ /**
231
+ * Defines categories for organizing semantics in the UI.
232
+ */
233
+ export enum SemanticCategory {
234
+ /**
235
+ * User management, authentication, and access control
236
+ */
237
+ Identity = 'Identity & Authentication',
238
+ /**
239
+ * Timestamps, versioning, and record lifecycle
240
+ */
241
+ Lifecycle = 'Timestamps & Versioning',
242
+ /**
243
+ * Text content, media, and rich content types
244
+ */
245
+ Content = 'Content & Media',
246
+ /**
247
+ * Business-specific data like pricing, inventory, status
248
+ */
249
+ Business = 'Business Data',
250
+ /**
251
+ * Contact information and communication
252
+ */
253
+ Contact = 'Contact Information',
254
+ /**
255
+ * Organization, categorization, and tagging
256
+ */
257
+ Organization = 'Classification & Organization',
258
+ /**
259
+ * Location and geographical data
260
+ */
261
+ Location = 'Location & Geography',
262
+ /**
263
+ * Calculated and derived values
264
+ */
265
+ Computed = 'Computed Values',
266
+ }
267
+
230
268
  /**
231
269
  * A base interface for all Data Semantics, containing common properties.
232
270
  * A semantic is an annotation applied to a Data Entity, Property, or Association
@@ -250,6 +288,10 @@ interface BaseDataSemantic {
250
288
  * Specifies whether the semantic applies to an Entity, Property, or Association.
251
289
  */
252
290
  scope: SemanticScope
291
+ /**
292
+ * The category this semantic belongs to for UI organization.
293
+ */
294
+ category: SemanticCategory
253
295
  }
254
296
 
255
297
  /**
@@ -307,213 +349,296 @@ export type DataSemantic = EntitySemantic | PropertySemantic | AssociationSemant
307
349
  */
308
350
  export const DataSemantics: Record<SemanticType, DataSemantic> = {
309
351
  //
310
- // Entity-Level Definitions
352
+ // Identity & Authentication
311
353
  //
312
354
 
313
355
  [SemanticType.User]: {
314
356
  id: SemanticType.User,
315
357
  displayName: 'User Entity',
316
358
  scope: SemanticScope.Entity,
317
- description: 'Designates an entity that represents system users, crucial for authentication and authorization.',
359
+ description: 'System users and accounts',
360
+ category: SemanticCategory.Identity,
318
361
  },
319
-
320
- //
321
- // Property-Level Definitions
322
- //
323
-
324
362
  [SemanticType.Password]: {
325
363
  id: SemanticType.Password,
326
364
  displayName: 'User Password',
327
365
  scope: SemanticScope.Property,
328
- description:
329
- 'Annotates the field as the user password. The runtime should treat this field with special care, ensuring it is encrypted and not exposed in API responses.',
366
+ description: 'Secure password field',
367
+ category: SemanticCategory.Identity,
368
+ applicableDataTypes: ['string'],
369
+ },
370
+ [SemanticType.UserRole]: {
371
+ id: SemanticType.UserRole,
372
+ displayName: 'User Role Field',
373
+ scope: SemanticScope.Property,
374
+ description: 'User permissions and access level',
375
+ category: SemanticCategory.Identity,
330
376
  applicableDataTypes: ['string'],
331
377
  },
378
+ [SemanticType.ResourceOwnerIdentifier]: {
379
+ id: SemanticType.ResourceOwnerIdentifier,
380
+ displayName: 'Resource Owner Identifier',
381
+ scope: SemanticScope.Association,
382
+ description: 'Links record to owner user',
383
+ category: SemanticCategory.Identity,
384
+ },
385
+
386
+ //
387
+ // Timestamps & Versioning
388
+ //
389
+
332
390
  [SemanticType.CreatedTimestamp]: {
333
391
  id: SemanticType.CreatedTimestamp,
334
392
  displayName: 'Creation Timestamp',
335
393
  scope: SemanticScope.Property,
336
- description: "Marks a field as the one that contains the object's creation timestamp.",
394
+ description: 'When record was created',
395
+ category: SemanticCategory.Lifecycle,
337
396
  applicableDataTypes: ['datetime'],
338
397
  },
339
398
  [SemanticType.UpdatedTimestamp]: {
340
399
  id: SemanticType.UpdatedTimestamp,
341
400
  displayName: 'Update Timestamp',
342
401
  scope: SemanticScope.Property,
343
- description: "Marks a field as the field that contains object's last modification timestamp.",
402
+ description: 'When record was last modified',
403
+ category: SemanticCategory.Lifecycle,
344
404
  applicableDataTypes: ['datetime'],
345
405
  },
346
406
  [SemanticType.DeletedTimestamp]: {
347
407
  id: SemanticType.DeletedTimestamp,
348
408
  displayName: 'Soft Delete Timestamp',
349
409
  scope: SemanticScope.Property,
350
- description: "Marks a field as the field that contains object's deletion timestamp.",
410
+ description: 'When record was marked deleted',
411
+ category: SemanticCategory.Lifecycle,
351
412
  applicableDataTypes: ['datetime'],
352
413
  },
353
- [SemanticType.PublicUniqueName]: {
354
- id: SemanticType.PublicUniqueName,
355
- displayName: 'Public Unique Name (Slug)',
414
+ [SemanticType.DeletedFlag]: {
415
+ id: SemanticType.DeletedFlag,
416
+ displayName: 'Soft Delete Flag',
356
417
  scope: SemanticScope.Property,
357
- description: 'A user-friendly, unique public identifier for a resource, often used in URLs.',
358
- applicableDataTypes: ['string'],
418
+ description: 'Mark record as deleted',
419
+ category: SemanticCategory.Lifecycle,
420
+ applicableDataTypes: ['boolean'],
421
+ },
422
+ [SemanticType.Version]: {
423
+ id: SemanticType.Version,
424
+ displayName: 'Version Number',
425
+ scope: SemanticScope.Property,
426
+ description: 'Auto-incrementing version counter',
427
+ category: SemanticCategory.Lifecycle,
428
+ applicableDataTypes: ['number'],
359
429
  },
430
+
431
+ //
432
+ // Content & Media
433
+ //
434
+
360
435
  [SemanticType.Title]: {
361
436
  id: SemanticType.Title,
362
437
  displayName: 'Record Title',
363
438
  scope: SemanticScope.Property,
364
- description: 'A title for the record. Used as a source for the PublicUniqueName semantic.',
439
+ description: 'Main title or heading',
440
+ category: SemanticCategory.Content,
365
441
  applicableDataTypes: ['string'],
366
442
  },
367
- [SemanticType.UserRole]: {
368
- id: SemanticType.UserRole,
369
- displayName: 'User Role Field',
443
+ [SemanticType.Description]: {
444
+ id: SemanticType.Description,
445
+ displayName: 'Description',
370
446
  scope: SemanticScope.Property,
371
- description: "A text or enum field that defines the user's role for role-based authorization.",
447
+ description: 'Detailed description text',
448
+ category: SemanticCategory.Content,
372
449
  applicableDataTypes: ['string'],
373
450
  },
374
- [SemanticType.Status]: {
375
- id: SemanticType.Status,
376
- displayName: 'Record Status',
451
+ [SemanticType.Summary]: {
452
+ id: SemanticType.Summary,
453
+ displayName: 'Summary',
377
454
  scope: SemanticScope.Property,
378
- description: 'A text or enum field to represent the state of a record.',
455
+ description: 'Brief summary text',
456
+ category: SemanticCategory.Content,
379
457
  applicableDataTypes: ['string'],
380
458
  },
381
- [SemanticType.Version]: {
382
- id: SemanticType.Version,
383
- displayName: 'Version Number',
459
+ [SemanticType.Markdown]: {
460
+ id: SemanticType.Markdown,
461
+ displayName: 'Markdown Content',
384
462
  scope: SemanticScope.Property,
385
- description: 'An integer field that automatically increments on every update.',
386
- applicableDataTypes: ['number'],
463
+ description: 'Formatted text content',
464
+ category: SemanticCategory.Content,
465
+ applicableDataTypes: ['string'],
466
+ },
467
+ [SemanticType.HTML]: {
468
+ id: SemanticType.HTML,
469
+ displayName: 'HTML Content',
470
+ scope: SemanticScope.Property,
471
+ description: 'Rich HTML content',
472
+ category: SemanticCategory.Content,
473
+ applicableDataTypes: ['string'],
387
474
  },
388
475
  [SemanticType.ImageURL]: {
389
476
  id: SemanticType.ImageURL,
390
477
  displayName: 'Image URL',
391
478
  scope: SemanticScope.Property,
392
- description: 'Annotates a field that holds a reference to an image data via an URL.',
479
+ description: 'Link to image file',
480
+ category: SemanticCategory.Content,
393
481
  applicableDataTypes: ['string'],
394
482
  },
395
483
  [SemanticType.FileURL]: {
396
484
  id: SemanticType.FileURL,
397
485
  displayName: 'File URL',
398
486
  scope: SemanticScope.Property,
399
- description: 'Annotates a field that holds a reference to a file object via an URL (non-image binary data).',
487
+ description: 'Link to file attachment',
488
+ category: SemanticCategory.Content,
400
489
  applicableDataTypes: ['string'],
401
490
  },
402
- [SemanticType.DeletedFlag]: {
403
- id: SemanticType.DeletedFlag,
404
- displayName: 'Soft Delete Flag',
405
- scope: SemanticScope.Property,
406
- description: 'A boolean property that marks the object as deleted without physically removing it.',
407
- applicableDataTypes: ['boolean'],
408
- },
409
- [SemanticType.Markdown]: {
410
- id: SemanticType.Markdown,
411
- displayName: 'Markdown Content',
491
+
492
+ //
493
+ // Business Data
494
+ //
495
+
496
+ [SemanticType.Status]: {
497
+ id: SemanticType.Status,
498
+ displayName: 'Record Status',
412
499
  scope: SemanticScope.Property,
413
- description: 'A text field that contains markdown content.',
500
+ description: 'Current state of record',
501
+ category: SemanticCategory.Business,
414
502
  applicableDataTypes: ['string'],
415
503
  },
416
- [SemanticType.HTML]: {
417
- id: SemanticType.HTML,
418
- displayName: 'HTML Content',
504
+ [SemanticType.Price]: {
505
+ id: SemanticType.Price,
506
+ displayName: 'Price',
419
507
  scope: SemanticScope.Property,
420
- description: 'Annotates a field that contains HTML content.',
421
- applicableDataTypes: ['string'],
508
+ description: 'Monetary value with currency',
509
+ category: SemanticCategory.Business,
510
+ applicableDataTypes: ['number', 'string'],
422
511
  },
423
- [SemanticType.GeospatialCoordinates]: {
424
- id: SemanticType.GeospatialCoordinates,
425
- displayName: 'Geospatial Coordinates',
512
+ [SemanticType.SKU]: {
513
+ id: SemanticType.SKU,
514
+ displayName: 'SKU',
426
515
  scope: SemanticScope.Property,
427
- description: 'Annotates a field that holds geospatial coordinate data (latitude/longitude).',
516
+ description: 'Product identification code',
517
+ category: SemanticCategory.Business,
428
518
  applicableDataTypes: ['string'],
429
519
  },
520
+
521
+ //
522
+ // Contact Information
523
+ //
524
+
430
525
  [SemanticType.Email]: {
431
526
  id: SemanticType.Email,
432
527
  displayName: 'Email',
433
528
  scope: SemanticScope.Property,
434
- description: 'Annotates a field as an email address with validation and verification options.',
529
+ description: 'Email address',
530
+ category: SemanticCategory.Contact,
435
531
  applicableDataTypes: ['string'],
436
532
  },
437
533
  [SemanticType.Phone]: {
438
534
  id: SemanticType.Phone,
439
535
  displayName: 'Phone',
440
536
  scope: SemanticScope.Property,
441
- description: 'Annotates a field as a phone number with validation and formatting options.',
537
+ description: 'Phone number',
538
+ category: SemanticCategory.Contact,
442
539
  applicableDataTypes: ['string'],
443
540
  },
444
- [SemanticType.Price]: {
445
- id: SemanticType.Price,
446
- displayName: 'Price',
447
- scope: SemanticScope.Property,
448
- description: 'Annotates a field as a monetary value with currency support and precision control.',
449
- applicableDataTypes: ['number', 'string'],
450
- },
451
541
  [SemanticType.URL]: {
452
542
  id: SemanticType.URL,
453
543
  displayName: 'URL',
454
544
  scope: SemanticScope.Property,
455
- description: 'Annotates a field as a URL with validation and allowed protocols.',
545
+ description: 'Web address or link',
546
+ category: SemanticCategory.Contact,
456
547
  applicableDataTypes: ['string'],
457
548
  },
458
- [SemanticType.SKU]: {
459
- id: SemanticType.SKU,
460
- displayName: 'SKU',
549
+
550
+ //
551
+ // Classification & Organization
552
+ //
553
+
554
+ [SemanticType.PublicUniqueName]: {
555
+ id: SemanticType.PublicUniqueName,
556
+ displayName: 'Public Unique Name (Slug)',
461
557
  scope: SemanticScope.Property,
462
- description:
463
- 'Annotates a field as a Stock Keeping Unit (SKU). Enforces uniqueness at the database level, critical for product catalogs. Provides automatic validation and formatting for product identification codes.',
558
+ description: 'URL-friendly unique identifier',
559
+ category: SemanticCategory.Organization,
464
560
  applicableDataTypes: ['string'],
465
561
  },
466
- [SemanticType.Description]: {
467
- id: SemanticType.Description,
468
- displayName: 'Description',
469
- scope: SemanticScope.Property,
470
- description: 'Annotates a field as a long-form description.',
471
- applicableDataTypes: ['string'],
562
+ [SemanticType.Tags]: {
563
+ id: SemanticType.Tags,
564
+ displayName: 'Tags',
565
+ scope: SemanticScope.Association,
566
+ description: 'Enable tagging functionality',
567
+ category: SemanticCategory.Organization,
472
568
  },
473
- [SemanticType.Summary]: {
474
- id: SemanticType.Summary,
475
- displayName: 'Summary',
569
+ [SemanticType.Categories]: {
570
+ id: SemanticType.Categories,
571
+ displayName: 'Categories',
572
+ scope: SemanticScope.Association,
573
+ description: 'Enable categorization functionality',
574
+ category: SemanticCategory.Organization,
575
+ },
576
+
577
+ //
578
+ // Location & Geography
579
+ //
580
+
581
+ [SemanticType.GeospatialCoordinates]: {
582
+ id: SemanticType.GeospatialCoordinates,
583
+ displayName: 'Geospatial Coordinates',
476
584
  scope: SemanticScope.Property,
477
- description: 'Annotates a field as a short summary.',
585
+ description: 'Location coordinates',
586
+ category: SemanticCategory.Location,
478
587
  applicableDataTypes: ['string'],
479
588
  },
589
+
590
+ //
591
+ // Computed Values
592
+ //
593
+
480
594
  [SemanticType.Calculated]: {
481
595
  id: SemanticType.Calculated,
482
596
  displayName: 'Calculated',
483
597
  scope: SemanticScope.Property,
484
- description: 'Annotates a field as a calculated value based on a formula.',
598
+ description: 'Auto-calculated field value',
599
+ category: SemanticCategory.Computed,
485
600
  applicableDataTypes: ['string'],
486
601
  },
487
602
  [SemanticType.Derived]: {
488
603
  id: SemanticType.Derived,
489
604
  displayName: 'Derived',
490
605
  scope: SemanticScope.Property,
491
- description: 'Annotates a field as derived from other fields.',
606
+ description: 'Value derived from other fields',
607
+ category: SemanticCategory.Computed,
492
608
  applicableDataTypes: ['string'],
493
609
  },
610
+ }
494
611
 
495
- //
496
- // Association-Level Definitions
497
- //
612
+ /**
613
+ * Helper function to get all semantics grouped by category.
614
+ * Useful for organizing semantics in UI dropdowns and forms.
615
+ */
616
+ export const getSemanticsByCategory = (): Record<SemanticCategory, DataSemantic[]> => {
617
+ const result: Record<SemanticCategory, DataSemantic[]> = {
618
+ [SemanticCategory.Identity]: [],
619
+ [SemanticCategory.Lifecycle]: [],
620
+ [SemanticCategory.Content]: [],
621
+ [SemanticCategory.Business]: [],
622
+ [SemanticCategory.Contact]: [],
623
+ [SemanticCategory.Organization]: [],
624
+ [SemanticCategory.Location]: [],
625
+ [SemanticCategory.Computed]: [],
626
+ }
498
627
 
499
- [SemanticType.ResourceOwnerIdentifier]: {
500
- id: SemanticType.ResourceOwnerIdentifier,
501
- displayName: 'Resource Owner Identifier',
502
- scope: SemanticScope.Association,
503
- description: 'Links a resource to a "User" entity instance, indicating ownership for access control.',
504
- },
505
- [SemanticType.Tags]: {
506
- id: SemanticType.Tags,
507
- displayName: 'Tags',
508
- scope: SemanticScope.Association,
509
- description: 'Annotates an association as supporting tag functionality.',
510
- },
511
- [SemanticType.Categories]: {
512
- id: SemanticType.Categories,
513
- displayName: 'Categories',
514
- scope: SemanticScope.Association,
515
- description: 'Annotates an association as supporting category functionality.',
516
- },
628
+ Object.values(DataSemantics).forEach((semantic) => {
629
+ result[semantic.category].push(semantic)
630
+ })
631
+
632
+ return result
633
+ }
634
+
635
+ /**
636
+ * Helper function to get semantics for a specific category.
637
+ * @param category The category to filter by
638
+ * @returns Array of semantics in the specified category
639
+ */
640
+ export const getSemanticsByCategoryType = (category: SemanticCategory): DataSemantic[] => {
641
+ return Object.values(DataSemantics).filter((semantic) => semantic.category === category)
517
642
  }
518
643
 
519
644
  /**
@@ -8,7 +8,10 @@ import {
8
8
  type ExposedEntity,
9
9
  } from '../../../src/index.js'
10
10
 
11
- test.group('ApiModel.createSchema()', () => {
11
+ test.group('ApiModel.createSchema()', (g) => {
12
+ g.tests.forEach((test) => {
13
+ test.tags(['@modeling', '@api', '@schema'])
14
+ })
12
15
  test('creates a schema with default values', ({ assert }) => {
13
16
  const schema = ApiModel.createSchema()
14
17
  assert.equal(schema.kind, ApiModelKind)
@@ -64,7 +67,11 @@ test.group('ApiModel.createSchema()', () => {
64
67
  })
65
68
  })
66
69
 
67
- test.group('ApiModel.constructor()', () => {
70
+ test.group('ApiModel.constructor()', (g) => {
71
+ g.tests.forEach((test) => {
72
+ test.tags(['@modeling', '@api', '@creation'])
73
+ })
74
+
68
75
  test('creates an instance with default values', ({ assert }) => {
69
76
  const model = new ApiModel()
70
77
  assert.equal(model.kind, ApiModelKind)
@@ -133,7 +140,10 @@ test.group('ApiModel.constructor()', () => {
133
140
  })
134
141
  })
135
142
 
136
- test.group('ApiModel.toJSON()', () => {
143
+ test.group('ApiModel.toJSON()', (g) => {
144
+ g.tests.forEach((test) => {
145
+ test.tags(['@modeling', '@api', '@serialization'])
146
+ })
137
147
  test('serializes default values', ({ assert }) => {
138
148
  const model = new ApiModel()
139
149
  const json = model.toJSON()
@@ -181,7 +191,10 @@ test.group('ApiModel.toJSON()', () => {
181
191
  })
182
192
  })
183
193
 
184
- test.group('ApiModel.exposeEntity()', () => {
194
+ test.group('ApiModel.exposeEntity()', (g) => {
195
+ g.tests.forEach((test) => {
196
+ test.tags(['@modeling', '@api'])
197
+ })
185
198
  test('exposes a new entity', ({ assert }) => {
186
199
  const model = new ApiModel()
187
200
  const entityKey = 'new-entity'
@@ -228,7 +241,10 @@ test.group('ApiModel.exposeEntity()', () => {
228
241
  })
229
242
  })
230
243
 
231
- test.group('ApiModel.removeEntity()', () => {
244
+ test.group('ApiModel.removeEntity()', (g) => {
245
+ g.tests.forEach((test) => {
246
+ test.tags(['@modeling', '@api'])
247
+ })
232
248
  test('removes an existing entity', ({ assert }) => {
233
249
  const model = new ApiModel()
234
250
  const entityKey = 'entity-to-remove'
@@ -274,7 +290,10 @@ test.group('ApiModel.removeEntity()', () => {
274
290
  })
275
291
  })
276
292
 
277
- test.group('ApiModel.getExposedEntity()', () => {
293
+ test.group('ApiModel.getExposedEntity()', (g) => {
294
+ g.tests.forEach((test) => {
295
+ test.tags(['@modeling', '@api'])
296
+ })
278
297
  test('returns an existing exposed entity', ({ assert }) => {
279
298
  const model = new ApiModel()
280
299
  const entityKey = 'get-entity'