@api-client/core 0.18.47 → 0.18.49
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/mocking/ModelingMock.d.ts +2 -0
- package/build/src/mocking/ModelingMock.d.ts.map +1 -1
- package/build/src/mocking/ModelingMock.js +2 -0
- package/build/src/mocking/ModelingMock.js.map +1 -1
- package/build/src/mocking/lib/File.d.ts +1 -0
- package/build/src/mocking/lib/File.d.ts.map +1 -1
- package/build/src/mocking/lib/File.js +1 -0
- package/build/src/mocking/lib/File.js.map +1 -1
- package/build/src/mocking/lib/Permission.d.ts +35 -0
- package/build/src/mocking/lib/Permission.d.ts.map +1 -0
- package/build/src/mocking/lib/Permission.js +89 -0
- package/build/src/mocking/lib/Permission.js.map +1 -0
- package/build/src/modeling/ApiModel.d.ts +12 -4
- package/build/src/modeling/ApiModel.d.ts.map +1 -1
- package/build/src/modeling/ApiModel.js +76 -31
- package/build/src/modeling/ApiModel.js.map +1 -1
- package/build/src/modeling/ExposedEntity.d.ts +9 -0
- package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
- package/build/src/modeling/ExposedEntity.js +23 -0
- package/build/src/modeling/ExposedEntity.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +9 -9
- package/package.json +3 -3
- package/src/mocking/ModelingMock.ts +2 -0
- package/src/mocking/lib/File.ts +1 -0
- package/src/mocking/lib/Permission.ts +100 -0
- package/src/modeling/ApiModel.ts +82 -37
- package/src/modeling/ExposedEntity.ts +28 -0
- package/tests/unit/mocking/current/Permission.spec.ts +285 -0
- package/tests/unit/modeling/api_model.spec.ts +20 -0
- package/tests/unit/modeling/api_model_expose_entity.spec.ts +25 -0
- package/tests/unit/modeling/api_model_remove_entity.spec.ts +17 -10
- package/tests/unit/modeling/exposed_entity_setter_validation.spec.ts +107 -0
|
@@ -42065,15 +42065,15 @@
|
|
|
42065
42065
|
"@id": "#191"
|
|
42066
42066
|
},
|
|
42067
42067
|
{
|
|
42068
|
-
"@id": "#200"
|
|
42069
|
-
},
|
|
42070
|
-
{
|
|
42071
42068
|
"@id": "#194"
|
|
42072
42069
|
},
|
|
42073
42070
|
{
|
|
42074
42071
|
"@id": "#197"
|
|
42075
42072
|
},
|
|
42076
42073
|
{
|
|
42074
|
+
"@id": "#200"
|
|
42075
|
+
},
|
|
42076
|
+
{
|
|
42077
42077
|
"@id": "#203"
|
|
42078
42078
|
},
|
|
42079
42079
|
{
|
|
@@ -43457,7 +43457,7 @@
|
|
|
43457
43457
|
"doc:ExternalDomainElement",
|
|
43458
43458
|
"doc:DomainElement"
|
|
43459
43459
|
],
|
|
43460
|
-
"doc:raw": "
|
|
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",
|
|
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": "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": "
|
|
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
|
{
|
|
@@ -44761,17 +44761,17 @@
|
|
|
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)-(10,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)-(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)-(
|
|
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.
|
|
4
|
+
"version": "0.18.49",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./browser.js": {
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"chalk": "^5.4.1",
|
|
100
100
|
"console-table-printer": "^2.11.2",
|
|
101
101
|
"dompurify": "^3.2.6",
|
|
102
|
-
"jsdom": "^
|
|
102
|
+
"jsdom": "^28.0.0",
|
|
103
103
|
"nanoid": "^5.1.5",
|
|
104
104
|
"tslog": "^4.9.3",
|
|
105
105
|
"ws": "^8.12.0",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"@japa/assert": "^4.0.1",
|
|
114
114
|
"@japa/browser-client": "^2.1.1",
|
|
115
115
|
"@japa/expect-type": "^2.0.3",
|
|
116
|
-
"@japa/runner": "^
|
|
116
|
+
"@japa/runner": "^5.3.0",
|
|
117
117
|
"@pawel-up/semver": "^0.1.4",
|
|
118
118
|
"@rollup/plugin-typescript": "^12.1.2",
|
|
119
119
|
"@types/chai-as-promised": "^8.0.2",
|
|
@@ -7,6 +7,7 @@ import { Invitation } from './lib/Invitation.js'
|
|
|
7
7
|
import { Trash } from './lib/Trash.js'
|
|
8
8
|
import { Patch } from './lib/Patch.js'
|
|
9
9
|
import { DataCatalog } from './lib/DataCatalog.js'
|
|
10
|
+
import { Permission } from './lib/Permission.js'
|
|
10
11
|
|
|
11
12
|
export class ModelingMock {
|
|
12
13
|
faker: Faker = faker
|
|
@@ -18,4 +19,5 @@ export class ModelingMock {
|
|
|
18
19
|
trash = new Trash()
|
|
19
20
|
patch = new Patch()
|
|
20
21
|
dataCatalog = new DataCatalog()
|
|
22
|
+
permission = new Permission()
|
|
21
23
|
}
|
package/src/mocking/lib/File.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { nanoid } from '../../nanoid.js'
|
|
|
6
6
|
export class File {
|
|
7
7
|
/**
|
|
8
8
|
* Generates a random file object.
|
|
9
|
+
* Note that this generator doesn't create the permissions object. Use Permission class for that.
|
|
9
10
|
* @param init Optional values to be present in the object.
|
|
10
11
|
* @returns Random file
|
|
11
12
|
*/
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker'
|
|
2
|
+
import { nanoid } from '../../nanoid.js'
|
|
3
|
+
import {
|
|
4
|
+
IPermission,
|
|
5
|
+
Kind as PermissionKind,
|
|
6
|
+
type PermissionRole,
|
|
7
|
+
type PermissionType,
|
|
8
|
+
type PermissionSourceRule,
|
|
9
|
+
} from '../../models/store/Permission.js'
|
|
10
|
+
|
|
11
|
+
export class Permission {
|
|
12
|
+
/**
|
|
13
|
+
* Generates a random permission object.
|
|
14
|
+
* @param init Configuration options for the permission object.
|
|
15
|
+
* @returns Random permission
|
|
16
|
+
*/
|
|
17
|
+
permission(init: Partial<IPermission> = {}): IPermission {
|
|
18
|
+
const type = init.type ?? faker.helpers.arrayElement(['user', 'group', 'organization'] as PermissionType[])
|
|
19
|
+
const role = init.role ?? faker.helpers.arrayElement(['reader', 'commenter', 'writer', 'owner'] as PermissionRole[])
|
|
20
|
+
|
|
21
|
+
const result: IPermission = {
|
|
22
|
+
kind: PermissionKind,
|
|
23
|
+
key: init.key ?? nanoid(),
|
|
24
|
+
type,
|
|
25
|
+
granteeId: init.granteeId ?? nanoid(),
|
|
26
|
+
itemId: init.itemId ?? nanoid(),
|
|
27
|
+
role,
|
|
28
|
+
addingUser: init.addingUser ?? nanoid(),
|
|
29
|
+
depth: init.depth ?? 0,
|
|
30
|
+
sourceRule:
|
|
31
|
+
init.sourceRule ??
|
|
32
|
+
faker.helpers.arrayElement([
|
|
33
|
+
'direct_user_grant',
|
|
34
|
+
'creator_default_owner',
|
|
35
|
+
'parent_owner_editor_rule',
|
|
36
|
+
] as PermissionSourceRule[]),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if ('displayName' in init) {
|
|
40
|
+
result.displayName = init.displayName
|
|
41
|
+
} else {
|
|
42
|
+
if (type === 'user') {
|
|
43
|
+
result.displayName = faker.person.fullName()
|
|
44
|
+
} else if (type === 'group') {
|
|
45
|
+
result.displayName = faker.lorem.word()
|
|
46
|
+
} else if (type === 'organization') {
|
|
47
|
+
result.displayName = faker.company.name()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ('expirationTime' in init) {
|
|
52
|
+
result.expirationTime = init.expirationTime
|
|
53
|
+
} else if (type === 'user' || type === 'group') {
|
|
54
|
+
result.expirationTime = faker.date.future().getTime()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generates a random user permission object.
|
|
62
|
+
* @param init Configuration options for the permission object.
|
|
63
|
+
* @returns Random user permission
|
|
64
|
+
*/
|
|
65
|
+
userPermission(init: Partial<IPermission> = {}): IPermission {
|
|
66
|
+
return this.permission({ ...init, type: 'user' })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generates a random group permission object.
|
|
71
|
+
* @param init Configuration options for the permission object.
|
|
72
|
+
* @returns Random group permission
|
|
73
|
+
*/
|
|
74
|
+
groupPermission(init: Partial<IPermission> = {}): IPermission {
|
|
75
|
+
return this.permission({ ...init, type: 'group' })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generates a random organization permission object.
|
|
80
|
+
* @param init Configuration options for the permission object.
|
|
81
|
+
* @returns Random organization permission
|
|
82
|
+
*/
|
|
83
|
+
organizationPermission(init: Partial<IPermission> = {}): IPermission {
|
|
84
|
+
return this.permission({ ...init, type: 'organization' })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generates a list of random permission objects.
|
|
89
|
+
* @param size The size of the returning array. Default is 25.
|
|
90
|
+
* @param init Configuration options for the permission object.
|
|
91
|
+
* @returns List of random permissions
|
|
92
|
+
*/
|
|
93
|
+
permissions(size = 25, init: Partial<IPermission> = {}): IPermission[] {
|
|
94
|
+
const result: IPermission[] = []
|
|
95
|
+
for (let i = 0; i < size; i++) {
|
|
96
|
+
result.push(this.permission(init))
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/modeling/ApiModel.ts
CHANGED
|
@@ -280,6 +280,16 @@ export class ApiModel extends DependentModel {
|
|
|
280
280
|
} else if (domain) {
|
|
281
281
|
throw new Error(`Invalid domain provided. Expected a DataDomain instance or schema.`)
|
|
282
282
|
}
|
|
283
|
+
// Note that since we're using the `DependentModel` class, but the API Model can have only one dependency,
|
|
284
|
+
// we keep the reference to the data domain under the `dependencyList` property.
|
|
285
|
+
// It is all handled by the parent class `DependentModel`. This way we simplify the dependency management (loading)
|
|
286
|
+
// process when the API model is loaded from the API.
|
|
287
|
+
if (domain) {
|
|
288
|
+
if (!domain.info.version) {
|
|
289
|
+
throw new Error(`Domain ${domain.key} must have a version.`)
|
|
290
|
+
}
|
|
291
|
+
init.dependencyList = [{ key: domain.key, version: domain.info.version }]
|
|
292
|
+
}
|
|
283
293
|
super(init.dependencyList, instances)
|
|
284
294
|
this.kind = init.kind
|
|
285
295
|
this.key = init.key
|
|
@@ -380,6 +390,12 @@ export class ApiModel extends DependentModel {
|
|
|
380
390
|
/**
|
|
381
391
|
* Exposes a new entity in the API model.
|
|
382
392
|
* If the entity already exists, it returns the existing one.
|
|
393
|
+
*
|
|
394
|
+
* The logic regarding exposing an entity:
|
|
395
|
+
* - If the entity is already exposed as a root entity, it returns the existing one.
|
|
396
|
+
* - If the entity has an association, we expose that entity as a nested entity (by setting the parent property).
|
|
397
|
+
* - If the associated entity is already exposed as a root entity, we do not follow the association.
|
|
398
|
+
*
|
|
383
399
|
* @param entity The entity key and domain to expose.
|
|
384
400
|
* @returns The exposed entity.
|
|
385
401
|
*/
|
|
@@ -403,8 +419,18 @@ export class ApiModel extends DependentModel {
|
|
|
403
419
|
}
|
|
404
420
|
const name = domainEntity.info.name || ''
|
|
405
421
|
const segment = pluralize(name.toLocaleLowerCase())
|
|
406
|
-
|
|
407
|
-
|
|
422
|
+
let relativeCollectionPath = `/${segment}`
|
|
423
|
+
let relativeResourcePath = `/${segment}/{id}`
|
|
424
|
+
|
|
425
|
+
// Check for root path collision and resolve by appending a number
|
|
426
|
+
let counter = 1
|
|
427
|
+
const originalCollectionPath = relativeCollectionPath
|
|
428
|
+
while (this.exposes.some((e) => e.isRoot && e.collectionPath === relativeCollectionPath)) {
|
|
429
|
+
relativeCollectionPath = `${originalCollectionPath}-${counter}`
|
|
430
|
+
relativeResourcePath = `${relativeCollectionPath}/{id}`
|
|
431
|
+
counter++
|
|
432
|
+
}
|
|
433
|
+
|
|
408
434
|
const newEntity: ExposedEntitySchema = {
|
|
409
435
|
kind: ExposedEntityKind,
|
|
410
436
|
key: nanoid(),
|
|
@@ -444,12 +470,7 @@ export class ApiModel extends DependentModel {
|
|
|
444
470
|
return
|
|
445
471
|
}
|
|
446
472
|
const maxDepth = options.maxDepth ?? 6
|
|
447
|
-
const
|
|
448
|
-
// Add parent entity's key to the visited set so we won't skip it when traversing
|
|
449
|
-
// associations.
|
|
450
|
-
visited.add(createDomainKey(parentExposure.entity))
|
|
451
|
-
|
|
452
|
-
const follow = (currentEntity: AssociationTarget, parentKey: string, depth: number) => {
|
|
473
|
+
const follow = (currentEntity: AssociationTarget, parentKey: string, depth: number, currentPath: string[]) => {
|
|
453
474
|
// Find the domain entity
|
|
454
475
|
const domainEntity = domain.findEntity(currentEntity.key, currentEntity.domain)
|
|
455
476
|
if (!domainEntity) return
|
|
@@ -462,24 +483,45 @@ export class ApiModel extends DependentModel {
|
|
|
462
483
|
continue
|
|
463
484
|
}
|
|
464
485
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
continue
|
|
486
|
+
const targetKeys = createDomainKey(target)
|
|
487
|
+
// Circular dependency check: if this entity is already in our *current* traversal path
|
|
488
|
+
if (currentPath.includes(targetKeys)) {
|
|
489
|
+
continue
|
|
469
490
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
491
|
+
|
|
492
|
+
// Check if this entity is ALREADY exposed anywhere in the model
|
|
493
|
+
const existingExposure = this.getExposedEntity(target)
|
|
494
|
+
|
|
495
|
+
if (existingExposure) {
|
|
496
|
+
// If it's already exposed and NOT root (i.e., it's currently a nested child of someone else),
|
|
497
|
+
// promote it to ROOT to avoid duplicating it or deeply nesting it in multiple places.
|
|
498
|
+
if (!existingExposure.isRoot) {
|
|
499
|
+
// 1. Calculate new root paths (handling collisions)
|
|
500
|
+
const targetDomEntity = domain.findEntity(target.key, target.domain)
|
|
501
|
+
if (targetDomEntity) {
|
|
502
|
+
const name = targetDomEntity.info.name || ''
|
|
503
|
+
const segment = pluralize(name.toLocaleLowerCase())
|
|
504
|
+
let relativeCollectionPath = `/${segment}`
|
|
505
|
+
let relativeResourcePath = `/${segment}/{id}`
|
|
506
|
+
|
|
507
|
+
let counter = 1
|
|
508
|
+
const originalCollectionPath = relativeCollectionPath
|
|
509
|
+
while (this.exposes.some((e) => e.isRoot && e.collectionPath === relativeCollectionPath)) {
|
|
510
|
+
relativeCollectionPath = `${originalCollectionPath}-${counter}`
|
|
511
|
+
relativeResourcePath = `${relativeCollectionPath}/{id}`
|
|
512
|
+
counter++
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 2. Update properties to make it root
|
|
516
|
+
existingExposure.isRoot = true
|
|
517
|
+
existingExposure.parent = undefined
|
|
518
|
+
existingExposure.collectionPath = relativeCollectionPath
|
|
519
|
+
existingExposure.resourcePath = relativeResourcePath
|
|
520
|
+
existingExposure.hasCollection = true
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Whether it was already root or we just promoted it, we stop following here.
|
|
524
|
+
continue
|
|
483
525
|
}
|
|
484
526
|
|
|
485
527
|
// Find the target domain entity for path generation
|
|
@@ -516,27 +558,30 @@ export class ApiModel extends DependentModel {
|
|
|
516
558
|
nestedExposure.truncated = true
|
|
517
559
|
} else {
|
|
518
560
|
// Recursively follow associations
|
|
519
|
-
follow(target, nestedExposure.key, depth + 1)
|
|
561
|
+
follow(target, nestedExposure.key, depth + 1, [...currentPath, targetKeys])
|
|
520
562
|
}
|
|
521
563
|
}
|
|
522
564
|
}
|
|
523
565
|
}
|
|
524
566
|
|
|
525
567
|
// Start following from the root exposure
|
|
526
|
-
|
|
568
|
+
// Initial path contains the root entity itself
|
|
569
|
+
const rootKey = createDomainKey(parentExposure.entity)
|
|
570
|
+
follow(parentExposure.entity, parentExposure.key, 0, [rootKey])
|
|
527
571
|
}
|
|
528
572
|
|
|
529
573
|
/**
|
|
530
574
|
* Removes an exposed entity from the API model.
|
|
531
|
-
*
|
|
575
|
+
* This also removes any nested exposed entities that are children of the removed entity.
|
|
576
|
+
*
|
|
577
|
+
* @param key The key of the exposed entity to remove.
|
|
532
578
|
*/
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
if (
|
|
536
|
-
|
|
579
|
+
removeExposedEntity(key: string): void {
|
|
580
|
+
const index = this.exposes.findIndex((e) => e.key === key)
|
|
581
|
+
if (index < 0) {
|
|
582
|
+
throw new Error(`Exposed entity with key "${key}" not found.`)
|
|
537
583
|
}
|
|
538
|
-
this.removeWithChildren(
|
|
539
|
-
this.notifyChange()
|
|
584
|
+
this.removeWithChildren(key)
|
|
540
585
|
}
|
|
541
586
|
|
|
542
587
|
private removeWithChildren(key: string): void {
|
|
@@ -573,11 +618,11 @@ export class ApiModel extends DependentModel {
|
|
|
573
618
|
}
|
|
574
619
|
|
|
575
620
|
/**
|
|
576
|
-
* Clears the API model for a new
|
|
621
|
+
* Clears the API model for a new data domain change.
|
|
577
622
|
* This method resets the dependencies, exposes, user,
|
|
578
623
|
* authentication, authorization, and session properties.
|
|
579
624
|
*/
|
|
580
|
-
|
|
625
|
+
cleanForDomainChange(): void {
|
|
581
626
|
this.dependencies.clear()
|
|
582
627
|
this.dependencyList = []
|
|
583
628
|
this.exposes = []
|
|
@@ -606,7 +651,7 @@ export class ApiModel extends DependentModel {
|
|
|
606
651
|
if (!domain.info.version) {
|
|
607
652
|
throw new Error(`Cannot attach DataDomain without a version. Please set the version in the domain info.`)
|
|
608
653
|
}
|
|
609
|
-
this.
|
|
654
|
+
this.cleanForDomainChange()
|
|
610
655
|
this.dependencies.set(domain.key, domain)
|
|
611
656
|
this.dependencyList = [{ key: domain.key, version: domain.info.version }]
|
|
612
657
|
this.notifyChange()
|
|
@@ -16,6 +16,15 @@ import type {
|
|
|
16
16
|
/**
|
|
17
17
|
* A class that specializes in representing an exposed Data Entity within an API Model.
|
|
18
18
|
*
|
|
19
|
+
* ## Design Note
|
|
20
|
+
* This class enforces strict path constraints (e.g., single-segment collection paths like `/users`,
|
|
21
|
+
* two-segment resource paths like `/users/{id}`).
|
|
22
|
+
* This is an intentional design choice to support a UI paradigm for non-technical users, ensuring that
|
|
23
|
+
* path segments are configured individually at each level of the exposure hierarchy.
|
|
24
|
+
*
|
|
25
|
+
* Flexibility is achieved by chaining exposed entities (parent/child relationships), where the final
|
|
26
|
+
* absolute path is composed of all ancestral paths. See `getAbsoluteResourcePath()` and `getAbsoluteCollectionPath()`.
|
|
27
|
+
*
|
|
19
28
|
* @fires change - Emitted when the exposed entity has changed.
|
|
20
29
|
*/
|
|
21
30
|
export class ExposedEntity extends EventTarget {
|
|
@@ -250,6 +259,17 @@ export class ExposedEntity extends EventTarget {
|
|
|
250
259
|
throw new Error(`Collection path must contain exactly one segment. Received: "${path}"`)
|
|
251
260
|
}
|
|
252
261
|
const normalizedCollection = `/${segments[0]}`
|
|
262
|
+
|
|
263
|
+
// Check for collision if this is a root entity
|
|
264
|
+
if (this.isRoot) {
|
|
265
|
+
const collision = this.api.exposes.find(
|
|
266
|
+
(e) => e.isRoot && e.key !== this.key && e.collectionPath === normalizedCollection
|
|
267
|
+
)
|
|
268
|
+
if (collision) {
|
|
269
|
+
throw new Error(`Collection path "${normalizedCollection}" is already in use by another root entity.`)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
253
273
|
// Preserve current parameter name if present, otherwise default to {id}
|
|
254
274
|
let param = '{id}'
|
|
255
275
|
if (this.resourcePath) {
|
|
@@ -309,6 +329,14 @@ export class ExposedEntity extends EventTarget {
|
|
|
309
329
|
`Resource path must contain exactly two segments when no collection is present. Received: "${cleaned}"`
|
|
310
330
|
)
|
|
311
331
|
}
|
|
332
|
+
// Check for collision if this is a root entity (singleton case)
|
|
333
|
+
if (this.isRoot) {
|
|
334
|
+
const collision = this.api.exposes.find((e) => e.isRoot && e.key !== this.key && e.resourcePath === cleaned)
|
|
335
|
+
if (collision) {
|
|
336
|
+
throw new Error(`Resource path "${cleaned}" is already in use by another root entity.`)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
312
340
|
if (this.resourcePath !== cleaned) {
|
|
313
341
|
this.resourcePath = `/${segments[0]}/${segments[1]}`
|
|
314
342
|
}
|