@api-client/core 0.20.5 → 0.20.6

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 (33) hide show
  1. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  2. package/build/src/modeling/ApiModel.js +9 -2
  3. package/build/src/modeling/ApiModel.js.map +1 -1
  4. package/build/src/modeling/DataDomain.d.ts +9 -3
  5. package/build/src/modeling/DataDomain.d.ts.map +1 -1
  6. package/build/src/modeling/DataDomain.js +25 -6
  7. package/build/src/modeling/DataDomain.js.map +1 -1
  8. package/build/src/modeling/DependentModel.d.ts +4 -0
  9. package/build/src/modeling/DependentModel.d.ts.map +1 -1
  10. package/build/src/modeling/DependentModel.js.map +1 -1
  11. package/build/src/modeling/DomainEntity.d.ts +20 -0
  12. package/build/src/modeling/DomainEntity.d.ts.map +1 -1
  13. package/build/src/modeling/DomainEntity.js +93 -0
  14. package/build/src/modeling/DomainEntity.js.map +1 -1
  15. package/build/src/modeling/RuntimeApiModel.d.ts +22 -0
  16. package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -1
  17. package/build/src/modeling/RuntimeApiModel.js +102 -1
  18. package/build/src/modeling/RuntimeApiModel.js.map +1 -1
  19. package/build/src/modeling/types.d.ts +15 -0
  20. package/build/src/modeling/types.d.ts.map +1 -1
  21. package/build/src/modeling/types.js.map +1 -1
  22. package/build/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +1 -1
  24. package/src/modeling/ApiModel.ts +8 -2
  25. package/src/modeling/DataDomain.ts +27 -7
  26. package/src/modeling/DependentModel.ts +4 -0
  27. package/src/modeling/DomainEntity.ts +100 -0
  28. package/src/modeling/RuntimeApiModel.ts +124 -2
  29. package/src/modeling/types.ts +16 -0
  30. package/tests/unit/modeling/RuntimeApiModel.spec.ts +91 -0
  31. package/tests/unit/modeling/data_domain_entities.spec.ts +56 -0
  32. package/tests/unit/modeling/data_domain_serialization.spec.ts +11 -11
  33. package/tests/unit/modeling/domain_entity_associations.spec.ts +63 -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.20.5",
4
+ "version": "0.20.6",
5
5
  "license": "UNLICENSED",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -289,9 +289,15 @@ export class ApiModel extends DependentModel {
289
289
  const init = ApiModel.createSchema(state)
290
290
  const instances: DataDomain[] = []
291
291
  if (domain instanceof DataDomain) {
292
- instances.push(domain)
292
+ if (domain.readOnly) {
293
+ instances.push(domain)
294
+ } else {
295
+ // eslint-disable-next-line no-console
296
+ console.warn(`Domain ${domain.key} is not read only, cloning it. This may have performance implications.`)
297
+ instances.push(domain.clone({ readOnly: true }))
298
+ }
293
299
  } else if (typeof domain === 'object' && domain.kind === DataDomainKind) {
294
- instances.push(new DataDomain(domain))
300
+ instances.push(new DataDomain(domain, undefined, { readOnly: true }))
295
301
  } else if (domain) {
296
302
  throw new Exception(`Invalid domain provided. Expected a DataDomain instance or schema.`, {
297
303
  code: 'E_DOMAIN_INVALID',
@@ -10,13 +10,13 @@ import {
10
10
  } from '../models/kinds.js'
11
11
  import type {
12
12
  DeserializationIssue,
13
- DeserializationMode,
14
13
  DomainGraphEdge,
15
14
  DomainGraphNodeType,
16
15
  SerializedGraph,
17
16
  DomainSearchCriteria,
18
17
  DomainSearchResult,
19
18
  SearchableNodeType,
19
+ DataDomainOptions,
20
20
  } from './types.js'
21
21
  import { type DomainNamespaceSchema, DomainNamespace, type NamespaceOrderedItem } from './DomainNamespace.js'
22
22
  import { type DomainModelSchema, DomainModel } from './DomainModel.js'
@@ -142,6 +142,11 @@ export class DataDomain extends DependentModel {
142
142
  */
143
143
  #notifying = false
144
144
 
145
+ /**
146
+ * Whether the DataDomain is in read-only mode, making it immutable and enabling caching operations.
147
+ */
148
+ readonly readOnly: boolean
149
+
145
150
  /**
146
151
  * This is to keep it consistent with the domain elements.
147
152
  */
@@ -193,15 +198,22 @@ export class DataDomain extends DependentModel {
193
198
  *
194
199
  * @param state The previously serialized state of the graph.
195
200
  * @param dependencies An array of foreign data domains to register with this domain.
201
+ * @param options Initialization options for the data domain.
196
202
  */
197
- constructor(state?: Partial<DataDomainSchema>, dependencies: DomainDependency[] = [], mode?: DeserializationMode) {
203
+ constructor(state?: Partial<DataDomainSchema>, dependencies: DomainDependency[] = [], options?: DataDomainOptions) {
198
204
  const init = DataDomain.createSchema(state)
199
205
  const instances: DataDomain[] = []
200
206
  for (const dep of dependencies) {
201
207
  if (dep instanceof DataDomain) {
202
- instances.push(dep)
208
+ if (dep.readOnly) {
209
+ instances.push(dep)
210
+ } else {
211
+ // eslint-disable-next-line no-console
212
+ console.warn(`Domain ${dep.key} is not read only, cloning it. This may have performance implications.`)
213
+ instances.push(dep.clone({ readOnly: true }))
214
+ }
203
215
  } else if (typeof dep === 'object' && dep.kind === DataDomainKind && dep.key) {
204
- const domain = new DataDomain(dep)
216
+ const domain = new DataDomain(dep, undefined, { readOnly: true })
205
217
  instances.push(domain)
206
218
  } else {
207
219
  throw new Error(`Invalid foreign domain dependency: ${dep}`)
@@ -211,10 +223,13 @@ export class DataDomain extends DependentModel {
211
223
  this.kind = init.kind
212
224
  this.key = init.key
213
225
  this.info = new Thing(init.info)
226
+ this.readOnly = options?.readOnly || false
227
+
228
+ const mode = options?.mode || 'strict'
214
229
  const result = deserialize(this, {
215
230
  json: init.graph,
216
231
  dependencies: instances,
217
- mode: mode || 'strict',
232
+ mode,
218
233
  })
219
234
  this.graph = result.graph
220
235
  if (result.issues.length > 0 && mode === 'lenient') {
@@ -242,12 +257,17 @@ export class DataDomain extends DependentModel {
242
257
  * It serializes the current domain and its graph, then deserializes it into a new instance.
243
258
  * It also deeply clones all foreign domain dependencies recursively.
244
259
  *
260
+ * @param options Optional initialization options to override the cloned ones.
245
261
  * @returns A new DataDomain instance that is a deep copy of this one.
246
262
  */
247
- clone(): DataDomain {
263
+ clone(options?: DataDomainOptions): DataDomain {
248
264
  const state = this.toJSON()
249
265
  const dependencies = Array.from(this.dependencies.values()).map((dep) => dep.clone())
250
- return new DataDomain(state, dependencies)
266
+ const opts: DataDomainOptions = {
267
+ readOnly: this.readOnly,
268
+ ...options,
269
+ }
270
+ return new DataDomain(state, dependencies, opts)
251
271
  }
252
272
 
253
273
  /**
@@ -1,6 +1,10 @@
1
1
  import type { DataDomain, DataDomainSchema } from './DataDomain.js'
2
2
  import type { ForeignDomainDependency } from './types.js'
3
3
 
4
+ /**
5
+ * If a dependency is a domain instance it should always be in read-only mode.
6
+ * Otherwise a read-only clone will be created for performance reason.
7
+ */
4
8
  export type DomainDependency = DataDomain | DataDomainSchema
5
9
 
6
10
  export interface DependentModelSchema {
@@ -127,6 +127,26 @@ export class DomainEntity extends DomainElement {
127
127
  */
128
128
  override kind: typeof DomainEntityKind
129
129
 
130
+ /**
131
+ * Cached inherited properties.
132
+ */
133
+ #inheritedPropertiesCache?: DomainProperty[]
134
+
135
+ /**
136
+ * Cached parent entities arrays.
137
+ */
138
+ #allParentsCache?: Record<'up' | 'down', DomainEntity[]>
139
+
140
+ /**
141
+ * Cached inherited associations.
142
+ */
143
+ #inheritedAssociationsCache?: DomainAssociation[]
144
+
145
+ /**
146
+ * Cached semantics across the inheritance chain.
147
+ */
148
+ #allSemanticsCache?: AppliedDataSemantic[]
149
+
130
150
  /**
131
151
  * The description of the domain entity.
132
152
  */
@@ -363,6 +383,9 @@ export class DomainEntity extends DomainElement {
363
383
  * @returns The iterator over the list of all properties.
364
384
  */
365
385
  withInheritedProperties(): MapIterator<DomainProperty> {
386
+ if (this.root.readOnly && this.#inheritedPropertiesCache) {
387
+ return this.#inheritedPropertiesCache.values()
388
+ }
366
389
  const cache = new Map<string, DomainProperty>()
367
390
  const parents = this.listAllParents('up')
368
391
  for (const parent of parents) {
@@ -381,6 +404,9 @@ export class DomainEntity extends DomainElement {
381
404
  }
382
405
  cache.set(prop.info.name, prop)
383
406
  }
407
+ if (this.root.readOnly) {
408
+ this.#inheritedPropertiesCache = Array.from(cache.values())
409
+ }
384
410
  return cache.values()
385
411
  }
386
412
 
@@ -438,6 +464,9 @@ export class DomainEntity extends DomainElement {
438
464
  * @returns An array of parent `DomainEntity` instances, ordered according to the requested traversal direction.
439
465
  */
440
466
  listAllParents(direction: 'up' | 'down' = 'up'): DomainEntity[] {
467
+ if (this.root.readOnly && this.#allParentsCache?.[direction]) {
468
+ return this.#allParentsCache[direction]
469
+ }
441
470
  const visited = new Set<string>()
442
471
  const parents: DomainEntity[] = []
443
472
  const queue: DomainEntity[] = [...this.listParents()]
@@ -456,6 +485,13 @@ export class DomainEntity extends DomainElement {
456
485
  parents.reverse()
457
486
  }
458
487
 
488
+ if (this.root.readOnly) {
489
+ if (!this.#allParentsCache) {
490
+ this.#allParentsCache = {} as Record<'up' | 'down', DomainEntity[]>
491
+ }
492
+ this.#allParentsCache[direction] = parents
493
+ }
494
+
459
495
  return parents
460
496
  }
461
497
 
@@ -982,6 +1018,70 @@ export class DomainEntity extends DomainElement {
982
1018
  return this.semantics.some((s) => s.id === semanticId)
983
1019
  }
984
1020
 
1021
+ /**
1022
+ * Collects a list of all associations that affect this entity,
1023
+ * including all inherited from parents associations as well as
1024
+ * this entity's associations.
1025
+ *
1026
+ * The resulting list already accounts for an association override.
1027
+ * An association overrides another association if it is lower in the
1028
+ * inheritance chain. As such, an association defined on this entity
1029
+ * always overrides previously defined entities.
1030
+ * @returns The iterator over the list of all associations.
1031
+ */
1032
+ withInheritedAssociations(): MapIterator<DomainAssociation> {
1033
+ if (this.root.readOnly && this.#inheritedAssociationsCache) {
1034
+ return this.#inheritedAssociationsCache.values()
1035
+ }
1036
+ const cache = new Map<string, DomainAssociation>()
1037
+ const parents = this.listAllParents('up')
1038
+ for (const parent of parents) {
1039
+ for (const assoc of parent.associations) {
1040
+ if (!assoc.info.name) {
1041
+ continue
1042
+ }
1043
+ cache.set(assoc.info.name, assoc)
1044
+ }
1045
+ }
1046
+ for (const assoc of this.associations) {
1047
+ if (!assoc.info.name) {
1048
+ continue
1049
+ }
1050
+ cache.set(assoc.info.name, assoc)
1051
+ }
1052
+ if (this.root.readOnly) {
1053
+ this.#inheritedAssociationsCache = Array.from(cache.values())
1054
+ }
1055
+ return cache.values()
1056
+ }
1057
+
1058
+ /**
1059
+ * Lists all semantics applied to this entity and its inherited parents.
1060
+ * Semantics from this entity overwrite those inherited from parents.
1061
+ *
1062
+ * @returns An array of applied semantics.
1063
+ */
1064
+ listAllSemantics(): AppliedDataSemantic[] {
1065
+ if (this.root.readOnly && this.#allSemanticsCache) {
1066
+ return this.#allSemanticsCache
1067
+ }
1068
+ const semanticsMap = new Map<SemanticType, AppliedDataSemantic>()
1069
+ const parents = this.listAllParents('up')
1070
+ for (const parent of parents) {
1071
+ for (const semantic of parent.semantics) {
1072
+ semanticsMap.set(semantic.id, semantic)
1073
+ }
1074
+ }
1075
+ for (const semantic of this.semantics) {
1076
+ semanticsMap.set(semantic.id, semantic)
1077
+ }
1078
+ const result = Array.from(semanticsMap.values())
1079
+ if (this.root.readOnly) {
1080
+ this.#allSemanticsCache = result
1081
+ }
1082
+ return result
1083
+ }
1084
+
985
1085
  /**
986
1086
  * Generates a unique name by appending a number to the base name.
987
1087
  * @param baseName The base name to make unique.
@@ -4,9 +4,10 @@ import type { DataDomainSchema } from './DataDomain.js'
4
4
  import type { ActionKind } from './actions/index.js'
5
5
  import type { ExposedEntity } from './ExposedEntity.js'
6
6
  import type { Action } from './actions/Action.js'
7
- import { SemanticType } from './Semantics.js'
8
7
  import type { DomainEntity } from './DomainEntity.js'
9
8
  import type { DomainProperty } from './DomainProperty.js'
9
+ import type { DomainAssociation } from './DomainAssociation.js'
10
+ import { SemanticType } from './Semantics.js'
10
11
  import { AccessRule, AccessRuleExecutionPhase } from './rules/AccessRule.js'
11
12
  import { Exception } from '../exceptions/exception.js'
12
13
 
@@ -108,6 +109,60 @@ export class RuntimeApiModel extends ApiModel {
108
109
  return new Map(this.#sessionProperties)
109
110
  }
110
111
 
112
+ /**
113
+ * Cached properties that have at least one semantic, grouped by their entity.
114
+ */
115
+ readonly #semanticPropertiesCache = new Map<DomainEntity, DomainProperty[]>()
116
+
117
+ /**
118
+ * Cached properties that have at least one semantic, grouped by their entity and then by SemanticType.
119
+ */
120
+ readonly #propertiesBySemanticCache = new Map<DomainEntity, Map<SemanticType, DomainProperty[]>>()
121
+
122
+ /**
123
+ * Cached associations that have at least one semantic, grouped by their entity.
124
+ */
125
+ readonly #semanticAssociationsCache = new Map<DomainEntity, DomainAssociation[]>()
126
+
127
+ /**
128
+ * Cached associations that have at least one semantic, grouped by their entity and then by SemanticType.
129
+ */
130
+ readonly #associationsBySemanticCache = new Map<DomainEntity, Map<SemanticType, DomainAssociation[]>>()
131
+
132
+ /**
133
+ * Returns a readonly map of properties with semantics.
134
+ * Note, it creates a copy of the cached map to prevent modification of the internal state.
135
+ */
136
+ public get semanticPropertiesCache(): ReadonlyMap<DomainEntity, DomainProperty[]> {
137
+ return new Map(this.#semanticPropertiesCache)
138
+ }
139
+
140
+ /**
141
+ * Returns properties for a specific entity that have a specific semantic type.
142
+ * Provides O(1) lookup using the precomputed cache.
143
+ */
144
+ public getPropertiesBySemantic(entity: DomainEntity, semanticType: SemanticType): DomainProperty[] {
145
+ const semanticMap = this.#propertiesBySemanticCache.get(entity)
146
+ return semanticMap?.get(semanticType) || []
147
+ }
148
+
149
+ /**
150
+ * Returns a readonly map of associations with semantics.
151
+ * Note, it creates a copy of the cached map to prevent modification of the internal state.
152
+ */
153
+ public get semanticAssociationsCache(): ReadonlyMap<DomainEntity, DomainAssociation[]> {
154
+ return new Map(this.#semanticAssociationsCache)
155
+ }
156
+
157
+ /**
158
+ * Returns associations for a specific entity that have a specific semantic type.
159
+ * Provides O(1) lookup using the precomputed cache.
160
+ */
161
+ public getAssociationsBySemantic(entity: DomainEntity, semanticType: SemanticType): DomainAssociation[] {
162
+ const semanticMap = this.#associationsBySemanticCache.get(entity)
163
+ return semanticMap?.get(semanticType) || []
164
+ }
165
+
111
166
  constructor(schema: RuntimeApiModelSchema, domainSchema: DataDomainSchema) {
112
167
  super(schema, domainSchema)
113
168
 
@@ -118,6 +173,7 @@ export class RuntimeApiModel extends ApiModel {
118
173
  this.#cacheEntitiesAndProperties()
119
174
  this.#precomputeAccessRules()
120
175
  this.#precomputeSessionProperties()
176
+ this.#precomputeSemanticProperties()
121
177
  }
122
178
 
123
179
  #precomputeSessionProperties(): void {
@@ -133,6 +189,72 @@ export class RuntimeApiModel extends ApiModel {
133
189
  }
134
190
  }
135
191
 
192
+ #precomputeSemanticProperties(): void {
193
+ if (!this.domain) {
194
+ return
195
+ }
196
+
197
+ const processEntity = (entity: DomainEntity) => {
198
+ if (this.#semanticPropertiesCache.has(entity)) {
199
+ return
200
+ }
201
+
202
+ const propertiesWithSemantics: DomainProperty[] = []
203
+ const semanticPropsMap = new Map<SemanticType, DomainProperty[]>()
204
+
205
+ for (const prop of entity.withInheritedProperties()) {
206
+ if (Array.isArray(prop.semantics) && prop.semantics.length > 0) {
207
+ propertiesWithSemantics.push(prop)
208
+
209
+ for (const semantic of prop.semantics) {
210
+ let propsForSemantic = semanticPropsMap.get(semantic.id)
211
+ if (!propsForSemantic) {
212
+ propsForSemantic = []
213
+ semanticPropsMap.set(semantic.id, propsForSemantic)
214
+ }
215
+ propsForSemantic.push(prop)
216
+ }
217
+ }
218
+ }
219
+
220
+ if (propertiesWithSemantics.length > 0) {
221
+ this.#semanticPropertiesCache.set(entity, propertiesWithSemantics)
222
+ this.#propertiesBySemanticCache.set(entity, semanticPropsMap)
223
+ }
224
+
225
+ const associationsWithSemantics: DomainAssociation[] = []
226
+ const semanticAssocMap = new Map<SemanticType, DomainAssociation[]>()
227
+
228
+ for (const assoc of entity.withInheritedAssociations()) {
229
+ if (Array.isArray(assoc.semantics) && assoc.semantics.length > 0) {
230
+ associationsWithSemantics.push(assoc)
231
+
232
+ for (const semantic of assoc.semantics) {
233
+ let assocForSemantic = semanticAssocMap.get(semantic.id)
234
+ if (!assocForSemantic) {
235
+ assocForSemantic = []
236
+ semanticAssocMap.set(semantic.id, assocForSemantic)
237
+ }
238
+ assocForSemantic.push(assoc)
239
+ }
240
+ }
241
+ }
242
+
243
+ if (associationsWithSemantics.length > 0) {
244
+ this.#semanticAssociationsCache.set(entity, associationsWithSemantics)
245
+ this.#associationsBySemanticCache.set(entity, semanticAssocMap)
246
+ }
247
+ }
248
+
249
+ for (const entity of this.domain.listEntities()) {
250
+ processEntity(entity)
251
+ }
252
+
253
+ for (const entity of this.domain.listAllForeignEntities()) {
254
+ processEntity(entity)
255
+ }
256
+ }
257
+
136
258
  #precomputeAccessRules(): void {
137
259
  for (const entity of this.exposes.values()) {
138
260
  for (const action of entity.actions) {
@@ -228,7 +350,7 @@ export class RuntimeApiModel extends ApiModel {
228
350
 
229
351
  this.cachedEntities.user = userEntity
230
352
 
231
- for (const prop of userEntity.properties) {
353
+ for (const prop of userEntity.withInheritedProperties()) {
232
354
  if (prop.hasSemantic(SemanticType.Username)) {
233
355
  this.cachedProperties.username = prop
234
356
  }
@@ -974,3 +974,19 @@ export interface AssociationSchema {
974
974
  */
975
975
  unionType?: 'allOf' | 'anyOf' | 'oneOf' | 'not'
976
976
  }
977
+
978
+ /**
979
+ * Options used when initializing a DataDomain.
980
+ */
981
+ export interface DataDomainOptions {
982
+ /**
983
+ * The mode used during deserialization to control error tolerance.
984
+ * 'strict' throws errors on invalid references. 'lenient' collects them.
985
+ */
986
+ mode?: DeserializationMode
987
+ /**
988
+ * Whether the DataDomain is in read-only mode, which makes it immutable
989
+ * and enables internal caching optimizations.
990
+ */
991
+ readOnly?: boolean
992
+ }
@@ -179,6 +179,97 @@ test.group('RuntimeApiModel', () => {
179
179
  assert.equal(runtimeModel.sessionProperties.get(prop2.key)?.key, prop2.key)
180
180
  }).tags(['@modeling', '@runtime'])
181
181
 
182
+ test('precomputes semanticPropertiesCache correctly', ({ assert }) => {
183
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
184
+ const modelNode = domain.addModel({ key: 'users' })
185
+ const userEntity = modelNode.addEntity({ info: { name: 'User' } })
186
+ const prop1 = userEntity.addProperty({ info: { name: 'Prop1' }, semantics: [{ id: SemanticType.Password }] })
187
+ const prop2 = userEntity.addProperty({ info: { name: 'Prop2' }, semantics: [{ id: SemanticType.Username }] })
188
+ const prop3 = userEntity.addProperty({ info: { name: 'Prop3' } })
189
+
190
+ const schema = {
191
+ key: 'api-1',
192
+ info: { name: 'Test API' },
193
+ exposes: [
194
+ {
195
+ key: 'expose-1',
196
+ entity: { key: userEntity.key, domain: domain.key },
197
+ actions: [],
198
+ },
199
+ ],
200
+ routingMap: {},
201
+ }
202
+
203
+ const runtimeModel = new RuntimeApiModel(schema as any, domain.toJSON())
204
+
205
+ assert.equal(runtimeModel.semanticPropertiesCache.size, 1)
206
+
207
+ // Convert keys iterator to array to find the matched entity reference
208
+ const cachedEntities = Array.from(runtimeModel.semanticPropertiesCache.keys())
209
+ assert.equal(cachedEntities.length, 1)
210
+ assert.equal(cachedEntities[0].key, userEntity.key)
211
+
212
+ const cachedProps = runtimeModel.semanticPropertiesCache.get(cachedEntities[0])
213
+ assert.isDefined(cachedProps)
214
+ assert.equal(cachedProps?.length, 2)
215
+ assert.equal(cachedProps?.[0].key, prop1.key)
216
+ assert.equal(cachedProps?.[1].key, prop2.key)
217
+ }).tags(['@modeling', '@runtime'])
218
+
219
+ test('precomputes semanticAssociationsCache correctly', ({ assert }) => {
220
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
221
+ const modelNode = domain.addModel({ key: 'users' })
222
+ const userEntity = modelNode.addEntity({ info: { name: 'User' } })
223
+
224
+ const assoc1 = userEntity.addAssociation({ info: { name: 'Assoc1' }, targets: [] })
225
+ assoc1.semantics = [{ id: SemanticType.Tags }]
226
+
227
+ const assoc2 = userEntity.addAssociation({ info: { name: 'Assoc2' }, targets: [] })
228
+ assoc2.semantics = [{ id: SemanticType.Categories }]
229
+
230
+ const assoc3 = userEntity.addAssociation({ info: { name: 'Assoc3' }, targets: [] })
231
+
232
+ const schema = {
233
+ key: 'api-1',
234
+ info: { name: 'Test API' },
235
+ exposes: [
236
+ {
237
+ key: 'expose-1',
238
+ entity: { key: userEntity.key, domain: domain.key },
239
+ actions: [],
240
+ },
241
+ ],
242
+ routingMap: {},
243
+ }
244
+
245
+ const runtimeModel = new RuntimeApiModel(schema as any, domain.toJSON())
246
+
247
+ assert.equal(runtimeModel.semanticAssociationsCache.size, 1)
248
+
249
+ // Convert keys iterator to array to find the matched entity reference
250
+ const cachedEntities = Array.from(runtimeModel.semanticAssociationsCache.keys())
251
+ assert.equal(cachedEntities.length, 1)
252
+ assert.equal(cachedEntities[0].key, userEntity.key)
253
+
254
+ const cachedAssocs = runtimeModel.semanticAssociationsCache.get(cachedEntities[0])
255
+ assert.isDefined(cachedAssocs)
256
+ assert.equal(cachedAssocs?.length, 2)
257
+ assert.equal(cachedAssocs?.[0].key, assoc1.key)
258
+ assert.equal(cachedAssocs?.[1].key, assoc2.key)
259
+
260
+ // Test getAssociationsBySemantic
261
+ const tagsAssocs = runtimeModel.getAssociationsBySemantic(cachedEntities[0], SemanticType.Tags)
262
+ assert.lengthOf(tagsAssocs, 1)
263
+ assert.equal(tagsAssocs[0].key, assoc1.key)
264
+
265
+ const categoriesAssocs = runtimeModel.getAssociationsBySemantic(cachedEntities[0], SemanticType.Categories)
266
+ assert.lengthOf(categoriesAssocs, 1)
267
+ assert.equal(categoriesAssocs[0].key, assoc2.key)
268
+
269
+ const emptyAssocs = runtimeModel.getAssociationsBySemantic(cachedEntities[0], SemanticType.ResourceOwnerIdentifier)
270
+ assert.lengthOf(emptyAssocs, 0)
271
+ }).tags(['@modeling', '@runtime', '@semantic'])
272
+
182
273
  test('throws exception if session property is missing from domain', ({ assert }) => {
183
274
  const domain = new DataDomain({ info: { version: '1.0.0' } })
184
275
  const modelNode = domain.addModel({ key: 'users' })
@@ -575,3 +575,59 @@ test.group('DataDomain.moveEntity()', () => {
575
575
  assert.isTrue(dataDomain.graph.hasNode(assoc.key))
576
576
  }).tags(['@modeling', '@entity', '@move'])
577
577
  })
578
+
579
+ test.group('DomainEntity.listAllSemantics()', () => {
580
+ test('returns semantics from entity and its parents', ({ assert }) => {
581
+ const dataDomain = new DataDomain()
582
+ const model = dataDomain.addModel()
583
+ const parentEntity = model.addEntity({ key: 'parent' })
584
+ const childEntity = model.addEntity({ key: 'child' })
585
+
586
+ parentEntity.addSemantic({ id: SemanticType.User })
587
+ childEntity.addSemantic({ id: SemanticType.Address })
588
+
589
+ childEntity.addParent(parentEntity.key)
590
+
591
+ const inherited = childEntity.listAllSemantics()
592
+ assert.lengthOf(inherited, 2)
593
+ const semanticIds = inherited.map((s) => s.id).sort()
594
+ assert.deepEqual(semanticIds, [SemanticType.Address, SemanticType.User].sort())
595
+ }).tags(['@modeling', '@entity', '@semantic'])
596
+
597
+ test('overrides parent semantics with the same id', ({ assert }) => {
598
+ const dataDomain = new DataDomain()
599
+ const model = dataDomain.addModel()
600
+ const parentEntity = model.addEntity({ key: 'parent' })
601
+ const childEntity = model.addEntity({ key: 'child' })
602
+
603
+ parentEntity.addSemantic({ id: SemanticType.User, config: { value: 'parent-val' } })
604
+ childEntity.addSemantic({ id: SemanticType.User, config: { value: 'child-val' } })
605
+
606
+ childEntity.addParent(parentEntity.key)
607
+
608
+ const inherited = childEntity.listAllSemantics()
609
+ assert.lengthOf(inherited, 1)
610
+ assert.deepEqual(inherited[0].config, { value: 'child-val' })
611
+ }).tags(['@modeling', '@entity', '@semantic'])
612
+
613
+ test('uses cached inherited semantics when domain is readOnly', ({ assert }) => {
614
+ const dataDomain = new DataDomain(undefined, [], { readOnly: true })
615
+ const model = dataDomain.addModel()
616
+ const parentEntity = model.addEntity({ key: 'parent' })
617
+ const childEntity = model.addEntity({ key: 'child' })
618
+
619
+ parentEntity.addSemantic({ id: SemanticType.User })
620
+
621
+ childEntity.addParent(parentEntity.key)
622
+
623
+ const inherited1 = childEntity.listAllSemantics()
624
+ assert.lengthOf(inherited1, 1)
625
+
626
+ // Add another semantic bypassing normal immutable logic for test purposes
627
+ parentEntity.addSemantic({ id: SemanticType.Address })
628
+
629
+ const inherited2 = childEntity.listAllSemantics()
630
+ // Should still be length 1 due to the cache
631
+ assert.lengthOf(inherited2, 1)
632
+ }).tags(['@modeling', '@entity', '@semantic', '@readonly'])
633
+ })