@api-client/core 0.18.27 → 0.18.29
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.
- package/build/src/modeling/ApiModel.d.ts +18 -9
- package/build/src/modeling/ApiModel.d.ts.map +1 -1
- package/build/src/modeling/ApiModel.js +141 -13
- package/build/src/modeling/ApiModel.js.map +1 -1
- package/build/src/modeling/helpers/endpointHelpers.d.ts +2 -0
- package/build/src/modeling/helpers/endpointHelpers.d.ts.map +1 -0
- package/build/src/modeling/helpers/endpointHelpers.js +6 -0
- package/build/src/modeling/helpers/endpointHelpers.js.map +1 -0
- package/build/src/modeling/helpers/keying.d.ts +9 -0
- package/build/src/modeling/helpers/keying.d.ts.map +1 -0
- package/build/src/modeling/helpers/keying.js +10 -0
- package/build/src/modeling/helpers/keying.js.map +1 -0
- package/build/src/modeling/types.d.ts +78 -9
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +24 -24
- package/package.json +3 -2
- package/src/modeling/ApiModel.ts +159 -15
- package/src/modeling/helpers/endpointHelpers.ts +5 -0
- package/src/modeling/helpers/keying.ts +11 -0
- package/src/modeling/readme.md +153 -7
- package/src/modeling/types.ts +85 -9
- package/tests/unit/modeling/api_model.spec.ts +25 -137
- package/tests/unit/modeling/api_model_expose_entity.spec.ts +190 -0
- package/tests/unit/modeling/api_model_remove_entity.spec.ts +82 -0
- package/tests/unit/modeling/helpers/endpointHelpers.spec.ts +10 -0
|
@@ -42062,19 +42062,19 @@
|
|
|
42062
42062
|
"@id": "#209"
|
|
42063
42063
|
},
|
|
42064
42064
|
{
|
|
42065
|
-
"@id": "#
|
|
42065
|
+
"@id": "#200"
|
|
42066
42066
|
},
|
|
42067
42067
|
{
|
|
42068
|
-
"@id": "#
|
|
42068
|
+
"@id": "#203"
|
|
42069
42069
|
},
|
|
42070
42070
|
{
|
|
42071
|
-
"@id": "#
|
|
42071
|
+
"@id": "#191"
|
|
42072
42072
|
},
|
|
42073
42073
|
{
|
|
42074
|
-
"@id": "#
|
|
42074
|
+
"@id": "#197"
|
|
42075
42075
|
},
|
|
42076
42076
|
{
|
|
42077
|
-
"@id": "#
|
|
42077
|
+
"@id": "#194"
|
|
42078
42078
|
},
|
|
42079
42079
|
{
|
|
42080
42080
|
"@id": "#206"
|
|
@@ -42813,13 +42813,13 @@
|
|
|
42813
42813
|
"@id": "#210"
|
|
42814
42814
|
},
|
|
42815
42815
|
{
|
|
42816
|
-
"@id": "#
|
|
42816
|
+
"@id": "#219"
|
|
42817
42817
|
},
|
|
42818
42818
|
{
|
|
42819
|
-
"@id": "#
|
|
42819
|
+
"@id": "#213"
|
|
42820
42820
|
},
|
|
42821
42821
|
{
|
|
42822
|
-
"@id": "#
|
|
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": "
|
|
43439
|
+
"doc:raw": "code: '5'\ndescription: 'Limited company'\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": "
|
|
43460
|
+
"doc:raw": "code: 'J'\ndescription: 'Information and communication'\n",
|
|
43461
43461
|
"core:mediaType": "application/yaml",
|
|
43462
43462
|
"sourcemaps:sources": [
|
|
43463
43463
|
{
|
|
@@ -43478,7 +43478,7 @@
|
|
|
43478
43478
|
"doc:ExternalDomainElement",
|
|
43479
43479
|
"doc:DomainElement"
|
|
43480
43480
|
],
|
|
43481
|
-
"doc:raw": "
|
|
43481
|
+
"doc:raw": "class: '3'\ndescription: '150 - 300'\nnumberOfFte: 5500\nnumberOfEmployees: 5232\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": "
|
|
43502
|
+
"doc:raw": "countryCode: \"BE\"\ngraydonEnterpriseId: 1057155523\nregistrationId: \"0422319093\"\nvatNumber: \"BE0422319093\"\ngraydonCompanyId: \"0422319093\"\nisBranchOffice: false\n",
|
|
43503
43503
|
"core:mediaType": "application/yaml",
|
|
43504
43504
|
"sourcemaps:sources": [
|
|
43505
43505
|
{
|
|
@@ -43520,7 +43520,7 @@
|
|
|
43520
43520
|
"doc:ExternalDomainElement",
|
|
43521
43521
|
"doc:DomainElement"
|
|
43522
43522
|
],
|
|
43523
|
-
"doc:raw": "
|
|
43523
|
+
"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",
|
|
43524
43524
|
"core:mediaType": "application/yaml",
|
|
43525
43525
|
"sourcemaps:sources": [
|
|
43526
43526
|
{
|
|
@@ -44253,7 +44253,7 @@
|
|
|
44253
44253
|
"doc:ExternalDomainElement",
|
|
44254
44254
|
"doc:DomainElement"
|
|
44255
44255
|
],
|
|
44256
|
-
"doc:raw": "type: 'GENERAL'\
|
|
44256
|
+
"doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
|
|
44257
44257
|
"core:mediaType": "application/yaml",
|
|
44258
44258
|
"sourcemaps:sources": [
|
|
44259
44259
|
{
|
|
@@ -44274,7 +44274,7 @@
|
|
|
44274
44274
|
"doc:ExternalDomainElement",
|
|
44275
44275
|
"doc:DomainElement"
|
|
44276
44276
|
],
|
|
44277
|
-
"doc:raw": "
|
|
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:
|
|
44298
|
+
"doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '21'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)21 302099'\n",
|
|
44299
44299
|
"core:mediaType": "application/yaml",
|
|
44300
44300
|
"sourcemaps:sources": [
|
|
44301
44301
|
{
|
|
@@ -44756,27 +44756,27 @@
|
|
|
44756
44756
|
{
|
|
44757
44757
|
"@id": "#193/source-map/lexical/element_0",
|
|
44758
44758
|
"sourcemaps:element": "amf://id#193",
|
|
44759
|
-
"sourcemaps:value": "[(1,0)-(
|
|
44759
|
+
"sourcemaps:value": "[(1,0)-(3,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)-(
|
|
44764
|
+
"sourcemaps:value": "[(1,0)-(3,0)]"
|
|
44765
44765
|
},
|
|
44766
44766
|
{
|
|
44767
44767
|
"@id": "#199/source-map/lexical/element_0",
|
|
44768
44768
|
"sourcemaps:element": "amf://id#199",
|
|
44769
|
-
"sourcemaps:value": "[(1,0)-(
|
|
44769
|
+
"sourcemaps:value": "[(1,0)-(5,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)-(
|
|
44774
|
+
"sourcemaps:value": "[(1,0)-(7,0)]"
|
|
44775
44775
|
},
|
|
44776
44776
|
{
|
|
44777
44777
|
"@id": "#205/source-map/lexical/element_0",
|
|
44778
44778
|
"sourcemaps:element": "amf://id#205",
|
|
44779
|
-
"sourcemaps:value": "[(1,0)-(
|
|
44779
|
+
"sourcemaps:value": "[(1,0)-(10,0)]"
|
|
44780
44780
|
},
|
|
44781
44781
|
{
|
|
44782
44782
|
"@id": "#208/source-map/lexical/element_0",
|
|
@@ -45121,17 +45121,17 @@
|
|
|
45121
45121
|
{
|
|
45122
45122
|
"@id": "#215/source-map/lexical/element_0",
|
|
45123
45123
|
"sourcemaps:element": "amf://id#215",
|
|
45124
|
-
"sourcemaps:value": "[(1,0)-(
|
|
45124
|
+
"sourcemaps:value": "[(1,0)-(7,0)]"
|
|
45125
45125
|
},
|
|
45126
45126
|
{
|
|
45127
45127
|
"@id": "#218/source-map/lexical/element_0",
|
|
45128
45128
|
"sourcemaps:element": "amf://id#218",
|
|
45129
|
-
"sourcemaps:value": "[(1,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)-(
|
|
45134
|
+
"sourcemaps:value": "[(1,0)-(6,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.18.
|
|
4
|
+
"version": "0.18.29",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./browser.js": {
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"@api-client/graph": "^0.3.5",
|
|
88
88
|
"@api-client/json": "^0.2.0",
|
|
89
89
|
"@esm-bundle/chai": "^4.3.4-fix.0",
|
|
90
|
+
"@jarrodek/pluralize": "^1.0.2",
|
|
90
91
|
"@metrichor/jmespath": "^0.3.1",
|
|
91
92
|
"@pawel-up/csv": "^0.2.0",
|
|
92
93
|
"@pawel-up/data-mock": "^0.4.0",
|
|
@@ -96,7 +97,7 @@
|
|
|
96
97
|
"chalk": "^5.4.1",
|
|
97
98
|
"console-table-printer": "^2.11.2",
|
|
98
99
|
"dompurify": "^3.2.6",
|
|
99
|
-
"jsdom": "^
|
|
100
|
+
"jsdom": "^27.0.0",
|
|
100
101
|
"nanoid": "^5.1.5",
|
|
101
102
|
"tslog": "^4.9.3",
|
|
102
103
|
"ws": "^8.12.0",
|
package/src/modeling/ApiModel.ts
CHANGED
|
@@ -11,10 +11,13 @@ import type {
|
|
|
11
11
|
RolesBasedAccessControl,
|
|
12
12
|
SessionConfiguration,
|
|
13
13
|
UsernamePasswordConfiguration,
|
|
14
|
+
ExposeOptions,
|
|
14
15
|
} from './types.js'
|
|
15
16
|
import { DataDomain } from './DataDomain.js'
|
|
16
17
|
import { DependentModel, type DependentModelSchema, type DomainDependency } from './DependentModel.js'
|
|
17
18
|
import { observed, toRaw } from '../decorators/observed.js'
|
|
19
|
+
import pluralize from '@jarrodek/pluralize'
|
|
20
|
+
import { createDomainKey } from './helpers/keying.js'
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Contact information for the exposed API.
|
|
@@ -54,12 +57,12 @@ export interface ApiModelSchema extends DependentModelSchema {
|
|
|
54
57
|
*/
|
|
55
58
|
kind: typeof ApiModelKind
|
|
56
59
|
/**
|
|
57
|
-
* The unique key of the
|
|
60
|
+
* The unique key of the API model schema.
|
|
58
61
|
* This is a stable identifier that does not change across versions.
|
|
59
62
|
*/
|
|
60
63
|
key: string
|
|
61
64
|
/**
|
|
62
|
-
* Contains the name, display name, description, and the version of the
|
|
65
|
+
* Contains the name, display name, description, and the version of the API model schema.
|
|
63
66
|
*/
|
|
64
67
|
info: IThing
|
|
65
68
|
|
|
@@ -376,41 +379,182 @@ export class ApiModel extends DependentModel {
|
|
|
376
379
|
/**
|
|
377
380
|
* Exposes a new entity in the API model.
|
|
378
381
|
* If the entity already exists, it returns the existing one.
|
|
379
|
-
* @param
|
|
382
|
+
* @param entity The entity key and domain to expose.
|
|
380
383
|
* @returns The exposed entity.
|
|
381
384
|
*/
|
|
382
|
-
exposeEntity(
|
|
383
|
-
const
|
|
385
|
+
exposeEntity(entity: AssociationTarget, options?: ExposeOptions): ExposedEntity {
|
|
386
|
+
const domain = this.domain
|
|
387
|
+
if (!domain) {
|
|
388
|
+
throw new Error(`No domain attached to API model`)
|
|
389
|
+
}
|
|
390
|
+
// checks whether the entity is already exposed as a root exposure.
|
|
391
|
+
const existing = this.exposes.find(
|
|
392
|
+
(e) => e.isRoot && e.entity.key === entity.key && e.entity.domain === entity.domain
|
|
393
|
+
)
|
|
384
394
|
if (existing) {
|
|
395
|
+
// quietly return the existing exposure
|
|
396
|
+
// TBD: should we throw an error here?
|
|
385
397
|
return existing
|
|
386
398
|
}
|
|
399
|
+
const domainEntity = domain.findEntity(entity.key, entity.domain)
|
|
400
|
+
if (!domainEntity) {
|
|
401
|
+
throw new Error(`Entity not found in domain: ${entity.key}`)
|
|
402
|
+
}
|
|
403
|
+
const name = domainEntity.info.name || ''
|
|
387
404
|
const newEntity: ExposedEntity = {
|
|
388
|
-
key:
|
|
405
|
+
key: nanoid(),
|
|
406
|
+
entity: { ...entity },
|
|
389
407
|
actions: [],
|
|
408
|
+
isRoot: true,
|
|
409
|
+
path: pluralize(name.toLocaleLowerCase()),
|
|
410
|
+
}
|
|
411
|
+
if (options) {
|
|
412
|
+
newEntity.exposeOptions = { ...options }
|
|
390
413
|
}
|
|
391
414
|
this.exposes.push(newEntity)
|
|
415
|
+
|
|
416
|
+
// Follow associations if requested
|
|
417
|
+
if (options?.followAssociations) {
|
|
418
|
+
if (options?.maxDepth === undefined || options.maxDepth > 0) {
|
|
419
|
+
this.followEntityAssociations(newEntity, options)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
392
422
|
this.notifyChange()
|
|
393
423
|
return newEntity
|
|
394
424
|
}
|
|
395
425
|
|
|
396
426
|
/**
|
|
397
|
-
*
|
|
398
|
-
*
|
|
427
|
+
* Follows associations for a newly exposed entity if configured to do so.
|
|
428
|
+
* This creates nested exposures based on the entity's associations.
|
|
429
|
+
*
|
|
430
|
+
* @param parentExposure The root exposure to follow associations from
|
|
431
|
+
* @param options The expose options containing follow configuration
|
|
399
432
|
*/
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
if (
|
|
403
|
-
|
|
404
|
-
|
|
433
|
+
private followEntityAssociations(parentExposure: ExposedEntity, options: ExposeOptions): void {
|
|
434
|
+
const domain = this.domain
|
|
435
|
+
if (!domain) {
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
const maxDepth = options.maxDepth ?? 6
|
|
439
|
+
const visited = new Set<string>()
|
|
440
|
+
// Add parent entity's key to the visited set so we won't skip it when traversing
|
|
441
|
+
// associations.
|
|
442
|
+
visited.add(createDomainKey(parentExposure.entity))
|
|
443
|
+
|
|
444
|
+
const follow = (currentEntity: AssociationTarget, parentKey: string, depth: number) => {
|
|
445
|
+
// Find the domain entity
|
|
446
|
+
const domainEntity = domain.findEntity(currentEntity.key, currentEntity.domain)
|
|
447
|
+
if (!domainEntity) return
|
|
448
|
+
|
|
449
|
+
// Iterate through associations
|
|
450
|
+
for (const association of domainEntity.listAssociations()) {
|
|
451
|
+
for (const target of association.targets) {
|
|
452
|
+
// Skip self-referencing associations
|
|
453
|
+
if (target.key === currentEntity.key && target.domain === currentEntity.domain) {
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Create unique identifier for circular detection
|
|
458
|
+
const visitKey = createDomainKey(target)
|
|
459
|
+
if (visited.has(visitKey)) {
|
|
460
|
+
continue // Skip circular references
|
|
461
|
+
}
|
|
462
|
+
visited.add(visitKey)
|
|
463
|
+
|
|
464
|
+
// Check if this nested exposure already exists
|
|
465
|
+
const existingNested = this.exposes.find(
|
|
466
|
+
(e) =>
|
|
467
|
+
!e.isRoot &&
|
|
468
|
+
e.entity.key === target.key &&
|
|
469
|
+
e.entity.domain === target.domain &&
|
|
470
|
+
e.parent?.key === parentKey
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if (existingNested) {
|
|
474
|
+
continue // Already exposed under this parent
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Find the target domain entity for path generation
|
|
478
|
+
const targetDomainEntity = domain.findEntity(target.key, target.domain)
|
|
479
|
+
if (!targetDomainEntity) continue
|
|
480
|
+
|
|
481
|
+
const name = association.info.name || ''
|
|
482
|
+
// Create nested exposure
|
|
483
|
+
const nestedExposure: ExposedEntity = {
|
|
484
|
+
key: nanoid(),
|
|
485
|
+
entity: { ...target },
|
|
486
|
+
actions: [],
|
|
487
|
+
isRoot: false,
|
|
488
|
+
path: pluralize(name.toLocaleLowerCase()),
|
|
489
|
+
parent: {
|
|
490
|
+
key: parentKey,
|
|
491
|
+
association: {
|
|
492
|
+
key: association.key,
|
|
493
|
+
domain: currentEntity.domain,
|
|
494
|
+
},
|
|
495
|
+
depth: depth + 1,
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this.exposes.push(nestedExposure)
|
|
500
|
+
if (depth + 1 >= maxDepth) {
|
|
501
|
+
nestedExposure.truncated = true
|
|
502
|
+
} else {
|
|
503
|
+
// Recursively follow associations
|
|
504
|
+
follow(target, nestedExposure.key, depth + 1)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Start following from the root exposure
|
|
511
|
+
follow(parentExposure.entity, parentExposure.key, 0)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Removes an exposed entity from the API model.
|
|
516
|
+
* @param entity The entity to remove.
|
|
517
|
+
*/
|
|
518
|
+
removeEntity(entity: AssociationTarget): void {
|
|
519
|
+
const current = this.exposes.find((e) => e.entity.key === entity.key && e.entity.domain === entity.domain)
|
|
520
|
+
if (!current) {
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
this.removeWithChildren(current.key)
|
|
524
|
+
this.notifyChange()
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private removeWithChildren(key: string): void {
|
|
528
|
+
const index = this.exposes.findIndex((e) => e.key === key)
|
|
529
|
+
if (index < 0) {
|
|
530
|
+
return
|
|
405
531
|
}
|
|
532
|
+
// Remove the parent itself
|
|
533
|
+
this.exposes.splice(index, 1)
|
|
534
|
+
// Remove all children recursively
|
|
535
|
+
const removeChildren = (parentKey: string) => {
|
|
536
|
+
// Find all exposures whose parent.key matches parentKey
|
|
537
|
+
const children = this.exposes.filter((e) => e.parent?.key === parentKey)
|
|
538
|
+
for (const child of children) {
|
|
539
|
+
removeChildren(child.key)
|
|
540
|
+
const childIndex = this.exposes.findIndex((e) => e.key === child.key)
|
|
541
|
+
if (childIndex >= 0) {
|
|
542
|
+
this.exposes.splice(childIndex, 1)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// Then also remove children
|
|
547
|
+
removeChildren(key)
|
|
548
|
+
this.notifyChange()
|
|
406
549
|
}
|
|
550
|
+
|
|
407
551
|
/**
|
|
408
552
|
* Returns the exposed entity by its key.
|
|
409
553
|
* @param entityKey The key of the entity to find.
|
|
410
554
|
* @returns The exposed entity or undefined if not found.
|
|
411
555
|
*/
|
|
412
|
-
getExposedEntity(
|
|
413
|
-
return this.exposes.find((e) => e.key ===
|
|
556
|
+
getExposedEntity(entity: AssociationTarget): ExposedEntity | undefined {
|
|
557
|
+
return this.exposes.find((e) => e.entity.key === entity.key && e.entity.domain === entity.domain)
|
|
414
558
|
}
|
|
415
559
|
|
|
416
560
|
/**
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AssociationTarget } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a consistent key for a given association target within its domain.
|
|
5
|
+
*
|
|
6
|
+
* @param target The association target to create a key for.
|
|
7
|
+
* @returns A unique key for the association target.
|
|
8
|
+
*/
|
|
9
|
+
export function createDomainKey(target: AssociationTarget): string {
|
|
10
|
+
return target.domain ? `${target.domain}:${target.key}` : target.key
|
|
11
|
+
}
|
package/src/modeling/readme.md
CHANGED
|
@@ -122,13 +122,159 @@ Let's imagine a simple e-commerce domain:
|
|
|
122
122
|
- **Entity-Parent**: A `DomainEntity` can have `DomainEntity` instances as parents.
|
|
123
123
|
- **Namespace-Foreign**: A `DomainNamespace` can have references to `DomainNamespace` instances.
|
|
124
124
|
|
|
125
|
+
## API Modeling
|
|
126
|
+
|
|
127
|
+
Beyond data modeling, the system also provides **API Modeling** capabilities that allow you to define how your data domain is exposed as a secure, production-ready API.
|
|
128
|
+
|
|
129
|
+
### ApiModel
|
|
130
|
+
|
|
131
|
+
The `ApiModel` class extends the data modeling system to define how Data Entities from a `DataDomain` are exposed via HTTP APIs. It provides configuration for:
|
|
132
|
+
|
|
133
|
+
**Core API Configuration:**
|
|
134
|
+
|
|
135
|
+
- **Exposed Entities**: Select which Data Entities from your Data Domain should be accessible via the API
|
|
136
|
+
- **API Actions**: Define what operations (List, Read, Create, Update, Delete, Search) are available for each entity
|
|
137
|
+
- **User Entity**: Designate which Data Entity represents a "User" for authentication purposes
|
|
138
|
+
|
|
139
|
+
**Security & Authentication:**
|
|
140
|
+
|
|
141
|
+
- **Authentication**: Configure how users prove their identity (e.g., username/password)
|
|
142
|
+
- **Authorization**: Define what authenticated users are allowed to do (e.g., Role-Based Access Control)
|
|
143
|
+
- **Session Management**: Configure transport and payload for user sessions (JWT, cookies)
|
|
144
|
+
- **Access Rules**: Define fine-grained access control policies for entities and actions
|
|
145
|
+
|
|
146
|
+
**API Metadata:**
|
|
147
|
+
|
|
148
|
+
- **Contact Information**: API maintainer details
|
|
149
|
+
- **License Information**: Legal information about API usage
|
|
150
|
+
- **Terms of Service**: Link to API usage terms
|
|
151
|
+
- **Rate Limiting**: Protect the API from overuse and abuse
|
|
152
|
+
|
|
153
|
+
### How API Modeling Works with Data Modeling
|
|
154
|
+
|
|
155
|
+
The API modeling system builds on top of the data modeling foundation:
|
|
156
|
+
|
|
157
|
+
1. **Start with a Data Domain**: Create your `DataDomain` with entities, properties, and associations
|
|
158
|
+
2. **Create an API Model**: Instantiate an `ApiModel` and attach your `DataDomain`
|
|
159
|
+
3. **Expose Entities**: Select which entities should be available via the API using `exposeEntity()`
|
|
160
|
+
4. **Configure Security**: Set up authentication, authorization, and session management
|
|
161
|
+
5. **Define Actions**: Configure what operations are available for each exposed entity
|
|
162
|
+
6. **Set API Metadata**: Add contact info, license, terms of service
|
|
163
|
+
|
|
164
|
+
### Example: E-Commerce API
|
|
165
|
+
|
|
166
|
+
Building on the e-commerce data domain example:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// 1. Create the data domain (as shown in previous example)
|
|
170
|
+
const domain = new DataDomain({ key: 'ecommerce', info: { name: 'E-Commerce Platform' } })
|
|
171
|
+
const userModel = domain.addModel({ info: { name: 'User Management' } })
|
|
172
|
+
const userEntity = userModel.addEntity({
|
|
173
|
+
info: { name: 'user' },
|
|
174
|
+
semantics: [{ type: SemanticType.User }] // Mark as User entity
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// 2. Create an API model
|
|
178
|
+
const apiModel = new ApiModel({
|
|
179
|
+
info: { name: 'E-Commerce API', version: '1.0.0' },
|
|
180
|
+
termsOfService: 'https://example.com/terms',
|
|
181
|
+
contact: { name: 'API Team', email: 'api@example.com' },
|
|
182
|
+
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' }
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// 3. Attach the data domain
|
|
186
|
+
apiModel.attachDataDomain(domain)
|
|
187
|
+
|
|
188
|
+
// 4. Configure security
|
|
189
|
+
apiModel.user = { key: userEntity.key }
|
|
190
|
+
apiModel.authentication = { strategy: 'UsernamePassword', passwordKey: 'password' }
|
|
191
|
+
apiModel.authorization = { strategy: 'RBAC', roleKey: 'role' }
|
|
192
|
+
apiModel.session = {
|
|
193
|
+
secret: 'your-secure-secret',
|
|
194
|
+
properties: ['email', 'role'],
|
|
195
|
+
cookie: { enabled: true, kind: 'cookie', lifetime: '7d' }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 5. Expose entities and configure actions
|
|
199
|
+
const exposedUser = apiModel.exposeEntity(userEntity.key)
|
|
200
|
+
exposedUser.actions = [
|
|
201
|
+
{ type: 'Read', enabled: true },
|
|
202
|
+
{ type: 'Update', enabled: true },
|
|
203
|
+
{ type: 'Create', enabled: true }
|
|
204
|
+
]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Key API Modeling Concepts
|
|
208
|
+
|
|
209
|
+
#### ExposedEntity
|
|
210
|
+
|
|
211
|
+
Represents a Data Entity that is exposed via the API. Contains:
|
|
212
|
+
|
|
213
|
+
- **Key**: Reference to the Data Entity
|
|
214
|
+
- **Actions**: List of available API operations (CRUD, Search)
|
|
215
|
+
- **Access Rules**: Entity-specific access control
|
|
216
|
+
- **Rate Limiting**: Entity-specific rate limiting rules
|
|
217
|
+
|
|
218
|
+
#### API Actions
|
|
219
|
+
|
|
220
|
+
Standard RESTful operations that can be enabled for each entity:
|
|
221
|
+
|
|
222
|
+
- **List**: Retrieve collections of entities with filtering and pagination
|
|
223
|
+
- **Read**: Retrieve a single entity by ID
|
|
224
|
+
- **Create**: Create new entities
|
|
225
|
+
- **Update**: Modify existing entities
|
|
226
|
+
- **Delete**: Remove entities
|
|
227
|
+
- **Search**: Full-text or advanced search across entities
|
|
228
|
+
|
|
229
|
+
#### Authentication Configuration
|
|
230
|
+
|
|
231
|
+
Defines how users prove their identity:
|
|
232
|
+
|
|
233
|
+
- **UsernamePassword**: Traditional email/password authentication
|
|
234
|
+
- Extensible for future strategies (SSO, OAuth, etc.)
|
|
235
|
+
|
|
236
|
+
#### Authorization Configuration
|
|
237
|
+
|
|
238
|
+
Defines what authenticated users can do:
|
|
239
|
+
|
|
240
|
+
- **RBAC (Role-Based Access Control)**: Users have roles, permissions granted to roles
|
|
241
|
+
- Extensible for future strategies (PBAC - Permission-Based Access Control)
|
|
242
|
+
|
|
243
|
+
#### Session Configuration
|
|
244
|
+
|
|
245
|
+
Manages user session data:
|
|
246
|
+
|
|
247
|
+
- **Properties**: Which User entity properties to include in session
|
|
248
|
+
- **Transport**: How session data is transmitted (JWT tokens, cookies)
|
|
249
|
+
- **Security**: Encryption, lifetime, and security settings
|
|
250
|
+
|
|
251
|
+
### Business-First API Design
|
|
252
|
+
|
|
253
|
+
The API modeling system follows the same business-first philosophy as data modeling:
|
|
254
|
+
|
|
255
|
+
- **Semantic Awareness**: Uses semantic types to automatically configure security (e.g., Password properties are write-only)
|
|
256
|
+
- **Standard Compliance**: Generates OpenAPI-compliant specifications
|
|
257
|
+
- **Security by Default**: Requires explicit configuration for authentication, authorization, and sessions
|
|
258
|
+
- **User-Friendly**: Clear error messages and validation feedback
|
|
259
|
+
- **Production Ready**: Built-in support for rate limiting, access control, and monitoring
|
|
260
|
+
|
|
125
261
|
## Summary
|
|
126
262
|
|
|
127
|
-
This
|
|
263
|
+
This comprehensive modeling system provides both **Data Modeling** and **API Modeling** capabilities:
|
|
264
|
+
|
|
265
|
+
**Data Modeling** allows you to:
|
|
266
|
+
|
|
267
|
+
- **Organize**: Group related data into namespaces and models
|
|
268
|
+
- **Structure**: Define entities with properties and relationships
|
|
269
|
+
- **Reuse**: Inherit from other entities and reference foreign namespaces
|
|
270
|
+
- **Translate**: Define bindings to map the model to different formats
|
|
271
|
+
- **Validate**: Validate the data model definition
|
|
272
|
+
- **Generate**: Generate AMF shapes and examples
|
|
273
|
+
|
|
274
|
+
**API Modeling** extends this to:
|
|
128
275
|
|
|
129
|
-
- **
|
|
130
|
-
- **
|
|
131
|
-
- **
|
|
132
|
-
- **
|
|
133
|
-
- **
|
|
134
|
-
- **Generate**: Generate AMF shapes and examples.
|
|
276
|
+
- **Expose**: Selectively expose Data Entities as API resources
|
|
277
|
+
- **Secure**: Configure authentication, authorization, and session management
|
|
278
|
+
- **Control**: Define fine-grained access rules and rate limiting
|
|
279
|
+
- **Document**: Generate complete API documentation with business context
|
|
280
|
+
- **Deploy**: Create production-ready APIs with proper security and monitoring
|