@api-client/core 0.19.30 → 0.19.32

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 (55) hide show
  1. package/build/src/modeling/ApiModel.d.ts.map +1 -1
  2. package/build/src/modeling/ApiModel.js +0 -8
  3. package/build/src/modeling/ApiModel.js.map +1 -1
  4. package/build/src/modeling/DomainAssociation.d.ts +4 -4
  5. package/build/src/modeling/DomainAssociation.d.ts.map +1 -1
  6. package/build/src/modeling/DomainAssociation.js.map +1 -1
  7. package/build/src/modeling/RuntimeApiModel.d.ts +16 -0
  8. package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -1
  9. package/build/src/modeling/RuntimeApiModel.js +31 -0
  10. package/build/src/modeling/RuntimeApiModel.js.map +1 -1
  11. package/build/src/modeling/ai/domain_response_schema.d.ts +4 -2
  12. package/build/src/modeling/ai/domain_response_schema.d.ts.map +1 -1
  13. package/build/src/modeling/ai/domain_response_schema.js +1 -1
  14. package/build/src/modeling/ai/domain_response_schema.js.map +1 -1
  15. package/build/src/modeling/ai/types.d.ts +2 -3
  16. package/build/src/modeling/ai/types.d.ts.map +1 -1
  17. package/build/src/modeling/ai/types.js.map +1 -1
  18. package/build/src/modeling/amf/ShapeGenerator.js +1 -1
  19. package/build/src/modeling/amf/ShapeGenerator.js.map +1 -1
  20. package/build/src/modeling/generators/oas_312/OasGenerator.js +1 -1
  21. package/build/src/modeling/generators/oas_312/OasGenerator.js.map +1 -1
  22. package/build/src/modeling/generators/oas_312/OasSchemaGenerator.js +3 -3
  23. package/build/src/modeling/generators/oas_312/OasSchemaGenerator.js.map +1 -1
  24. package/build/src/modeling/generators/oas_320/OasSchemaGenerator.js +3 -3
  25. package/build/src/modeling/generators/oas_320/OasSchemaGenerator.js.map +1 -1
  26. package/build/src/modeling/types.d.ts +55 -15
  27. package/build/src/modeling/types.d.ts.map +1 -1
  28. package/build/src/modeling/types.js.map +1 -1
  29. package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -1
  30. package/build/src/modeling/validation/api_model_rules.js +49 -46
  31. package/build/src/modeling/validation/api_model_rules.js.map +1 -1
  32. package/build/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +1 -1
  34. package/src/modeling/ApiModel.ts +0 -10
  35. package/src/modeling/DomainAssociation.ts +4 -4
  36. package/src/modeling/RuntimeApiModel.ts +46 -0
  37. package/src/modeling/ai/domain_response_schema.ts +1 -1
  38. package/src/modeling/ai/types.ts +2 -3
  39. package/src/modeling/amf/ShapeGenerator.ts +1 -1
  40. package/src/modeling/generators/oas_312/OasGenerator.ts +1 -1
  41. package/src/modeling/generators/oas_312/OasSchemaGenerator.ts +3 -3
  42. package/src/modeling/generators/oas_320/OasSchemaGenerator.ts +3 -3
  43. package/src/modeling/readme.md +2 -2
  44. package/src/modeling/types.ts +56 -15
  45. package/src/modeling/validation/api_model_rules.ts +50 -48
  46. package/src/modeling/validation/api_model_validation_rules.md +2 -2
  47. package/tests/unit/modeling/RuntimeApiModel.spec.ts +31 -0
  48. package/tests/unit/modeling/amf/shape_generator.spec.ts +32 -14
  49. package/tests/unit/modeling/api_model.spec.ts +6 -6
  50. package/tests/unit/modeling/data_domain_change_observers.spec.ts +1 -1
  51. package/tests/unit/modeling/domain_asociation.spec.ts +18 -18
  52. package/tests/unit/modeling/domain_entity_example_generator_json.spec.ts +68 -23
  53. package/tests/unit/modeling/domain_entity_example_generator_xml.spec.ts +32 -9
  54. package/tests/unit/modeling/generators/OasGenerator.spec.ts +3 -5
  55. package/tests/unit/modeling/validation/api_model_rules.spec.ts +12 -12
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.19.30",
4
+ "version": "0.19.32",
5
5
  "license": "UNLICENSED",
6
6
  "exports": {
7
7
  "./browser.js": {
@@ -6,9 +6,7 @@ import type {
6
6
  AuthenticationStrategy,
7
7
  AuthorizationStrategy,
8
8
  ExposedEntitySchema,
9
- RolesBasedAccessControl,
10
9
  SessionConfiguration,
11
- UsernamePasswordConfiguration,
12
10
  ExposeOptions,
13
11
  OffsetPaginationStrategy,
14
12
  CursorPaginationStrategy,
@@ -668,14 +666,6 @@ export class ApiModel extends DependentModel {
668
666
  if (this.session) {
669
667
  this.session.properties = []
670
668
  }
671
- if (this.authentication && this.authentication.strategy === 'UsernamePassword') {
672
- const typed = this.authentication as UsernamePasswordConfiguration
673
- typed.passwordKey = undefined
674
- }
675
- if (this.authorization && this.authorization.strategy == 'RBAC') {
676
- const typed = this.authorization as RolesBasedAccessControl
677
- typed.roleKey = ''
678
- }
679
669
  }
680
670
 
681
671
  /**
@@ -8,7 +8,7 @@ import type { DomainEntity } from './DomainEntity.js'
8
8
  import type { Shapes } from '@api-client/amf-core'
9
9
  import type { AssociationBinding, AssociationBindings, AssociationWebBindings } from './Bindings.js'
10
10
  import { DomainAttributeAttribute, DomainAttributeAttributes } from './DataFormat.js'
11
- import type { AssociationTarget, DomainGraphEdge } from './types.js'
11
+ import type { AssociationSchema, AssociationTarget, DomainGraphEdge } from './types.js'
12
12
  import { ShapeGenerator } from './amf/ShapeGenerator.js'
13
13
  import { DataSemantics, isAssociationSemantic, type SemanticType, type AppliedDataSemantic } from './Semantics.js'
14
14
 
@@ -53,7 +53,7 @@ export interface DomainAssociationSchema extends DomainElementSchema {
53
53
  * referenced entities schemas. Note, changes in the referenced entities may not be propagated
54
54
  * to schemas altered by the user.
55
55
  */
56
- schema?: Shapes.IApiAssociationShape
56
+ schema?: AssociationSchema
57
57
  /**
58
58
  * The list of bindings for this property.
59
59
  *
@@ -153,7 +153,7 @@ export class DomainAssociation extends DomainElement {
153
153
  * The schema allowing to translate the model into a
154
154
  * specific format (like JSON, RAML, XML, etc.)
155
155
  */
156
- @observed({ deep: true }) accessor schema: Shapes.IApiAssociationShape | undefined
156
+ @observed({ deep: true }) accessor schema: AssociationSchema | undefined
157
157
 
158
158
  /**
159
159
  * The list of bindings for this property.
@@ -351,7 +351,7 @@ export class DomainAssociation extends DomainElement {
351
351
  * const schema = association.ensureSchema();
352
352
  * ```
353
353
  */
354
- ensureSchema(): Shapes.IApiAssociationShape {
354
+ ensureSchema(): AssociationSchema {
355
355
  if (!this.schema) {
356
356
  this.schema = {}
357
357
  }
@@ -4,6 +4,9 @@ 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
+ import type { DomainEntity } from './DomainEntity.js'
9
+ import type { DomainProperty } from './DomainProperty.js'
7
10
 
8
11
  /**
9
12
  * Identifies a specific exposed entity and its action kind.
@@ -55,12 +58,55 @@ export class RuntimeApiModel extends ApiModel {
55
58
  */
56
59
  #definitions = new WeakMap<RouteToken[], RouteDefinition>()
57
60
 
61
+ /**
62
+ * Cached references to commonly used entities for fast runtime lookup.
63
+ */
64
+ cachedEntities: {
65
+ user?: DomainEntity
66
+ } = {}
67
+
68
+ /**
69
+ * Cached references to commonly used properties for fast runtime lookup.
70
+ */
71
+ cachedProperties: {
72
+ username?: DomainProperty
73
+ password?: DomainProperty
74
+ role?: DomainProperty
75
+ } = {}
76
+
58
77
  constructor(schema: RuntimeApiModelSchema, domainSchema: DataDomainSchema) {
59
78
  super(schema, domainSchema)
60
79
 
61
80
  if (schema.routingMap) {
62
81
  this.#initializeRouter(schema.routingMap)
63
82
  }
83
+
84
+ this.#cacheEntitiesAndProperties()
85
+ }
86
+
87
+ #cacheEntitiesAndProperties() {
88
+ if (!this.user || !this.domain) {
89
+ return
90
+ }
91
+
92
+ const userEntity = this.domain.findEntity(this.user.key, this.user.domain)
93
+ if (!userEntity) {
94
+ return
95
+ }
96
+
97
+ this.cachedEntities.user = userEntity
98
+
99
+ for (const prop of userEntity.properties) {
100
+ if (prop.hasSemantic(SemanticType.Username)) {
101
+ this.cachedProperties.username = prop
102
+ }
103
+ if (prop.hasSemantic(SemanticType.Password)) {
104
+ this.cachedProperties.password = prop
105
+ }
106
+ if (prop.hasSemantic(SemanticType.UserRole)) {
107
+ this.cachedProperties.role = prop
108
+ }
109
+ }
64
110
  }
65
111
 
66
112
  #initializeRouter(routingMap: RoutingMap) {
@@ -94,7 +94,7 @@ const PropertySchema = {
94
94
  const AssociationSchemaShape = {
95
95
  type: Type.OBJECT,
96
96
  properties: {
97
- linked: { type: Type.BOOLEAN },
97
+ embedded: { type: Type.BOOLEAN, description: 'Whether the association should be embedded in the target entity' },
98
98
  unionType: { type: Type.STRING, description: 'Enum: allOf, anyOf, oneOf, not' },
99
99
  },
100
100
  }
@@ -1,10 +1,9 @@
1
- import type { Shapes } from '@api-client/amf-core'
2
1
  import type { Exception } from '../../exceptions/exception.js'
3
2
  import type { AiSessionSchema } from '../../models/AiSession.js'
4
3
  import type { DomainPropertyType } from '../DataFormat.js'
5
4
  import type { OnDeleteRule } from '../index.js'
6
5
  import { SemanticType } from '../Semantics.js'
7
- import type { AssociationTarget, PropertySchema } from '../types.js'
6
+ import type { AssociationSchema, AssociationTarget, PropertySchema } from '../types.js'
8
7
  import type { AiModelMessageSchema, AiModelMessage, AiUserMessageSchema } from '../../models/AiMessage.js'
9
8
 
10
9
  /**
@@ -150,7 +149,7 @@ export interface AiDomainAssociation {
150
149
  multiple?: boolean
151
150
  onDelete?: OnDeleteRule
152
151
  semantics?: AiDomainSemantic[]
153
- schema?: Shapes.IApiAssociationShape
152
+ schema?: AssociationSchema
154
153
  }
155
154
 
156
155
  /**
@@ -199,7 +199,7 @@ export class ShapeGenerator {
199
199
  */
200
200
  associationShape(input: DomainAssociation, visited: Set<string> = new Set<string>()): Shapes.IShapeUnion | undefined {
201
201
  const schema = input.schema
202
- if (schema && schema.linked) {
202
+ if (!schema?.embedded) {
203
203
  return this.createLinkedShape(input)
204
204
  }
205
205
  const items = this.associationUnion(input, visited)
@@ -439,7 +439,7 @@ export class OasGenerator {
439
439
  }
440
440
  const links: Record<string, LinkObject> = {}
441
441
  for (const assoc of domainEntity.associations) {
442
- if (assoc.schema?.linked === false) {
442
+ if (assoc.schema?.embedded === true) {
443
443
  continue
444
444
  }
445
445
  const targets = Array.from(assoc.listTargets())
@@ -105,12 +105,12 @@ export class OasSchemaGenerator {
105
105
  const targets = Array.from(assoc.listTargets())
106
106
  if (targets.length === 0) continue
107
107
 
108
- const linked = assoc.schema?.linked === true
108
+ const linked = !assoc.schema?.embedded
109
109
  const targetRefs: (SchemaObject | ReferenceObject)[] = []
110
110
 
111
111
  for (const targetEntity of targets) {
112
112
  if (linked) {
113
- // When an entity is linked then we treat it as a regular property, which value is a key to
113
+ // When an entity is not embedded then we treat it as a regular property, which value is a key to
114
114
  // the target entity. The consuming application has to construct the endpoint manually.
115
115
  const targetName = targetEntity.info.displayName || targetEntity.info.name || targetEntity.key
116
116
  targetRefs.push({
@@ -118,7 +118,7 @@ export class OasSchemaGenerator {
118
118
  description: `The ID of the linked ${targetName}.`,
119
119
  })
120
120
  } else {
121
- // If not linked, embed the resource as a reference to the schema.
121
+ // If "embedded", add the resource as a reference to the schema.
122
122
  if (targetEntity !== entity) {
123
123
  this.generateEntity(targetEntity)
124
124
  }
@@ -78,12 +78,12 @@ export class OasSchemaGenerator {
78
78
  const targets = Array.from(assoc.listTargets())
79
79
  if (targets.length === 0) continue
80
80
 
81
- const linked = assoc.schema?.linked === true
81
+ const linked = !assoc.schema?.embedded
82
82
  const targetRefs: (SchemaObject | ReferenceObject)[] = []
83
83
 
84
84
  for (const targetEntity of targets) {
85
85
  if (linked) {
86
- // When an entity is linked then we treat it as a regular property, which value is a key to
86
+ // When an entity is not embedded then we treat it as a regular property, which value is a key to
87
87
  // the target entity. The consuming application has to construct the endpoint manually.
88
88
  const targetName = targetEntity.info.displayName || targetEntity.info.name || targetEntity.key
89
89
  targetRefs.push({
@@ -91,7 +91,7 @@ export class OasSchemaGenerator {
91
91
  description: `The ID of the linked ${targetName}.`,
92
92
  })
93
93
  } else {
94
- // If not linked, embed the resource as a reference to the schema.
94
+ // If "embedded", add the resource as a reference to the schema.
95
95
  if (targetEntity !== entity) {
96
96
  this.generateEntity(targetEntity)
97
97
  }
@@ -187,8 +187,8 @@ apiModel.attachDataDomain(domain)
187
187
 
188
188
  // 4. Configure security
189
189
  apiModel.user = { key: userEntity.key }
190
- apiModel.authentication = { strategy: 'UsernamePassword', passwordKey: 'password' }
191
- apiModel.authorization = { strategy: 'RBAC', roleKey: 'role' }
190
+ apiModel.authentication = { strategy: 'UsernamePassword' }
191
+ apiModel.authorization = { strategy: 'RBAC' }
192
192
  apiModel.session = {
193
193
  secret: 'your-secure-secret',
194
194
  properties: ['email', 'role'],
@@ -393,14 +393,6 @@ export interface AuthorizationConfiguration {
393
393
 
394
394
  export interface RolesBasedAccessControl extends AuthorizationConfiguration {
395
395
  strategy: 'RBAC'
396
- /**
397
- * The property within the designated "User" entity that defines the user's role.
398
- * This field is used in access rules to grant permissions.
399
- *
400
- * This property must be marked with the "Role" data semantic in the Data Modeler.
401
- * It is required to publish the API.
402
- */
403
- roleKey: string
404
396
  }
405
397
 
406
398
  export type AuthorizationStrategy = RolesBasedAccessControl
@@ -424,13 +416,6 @@ export interface AuthenticationConfiguration {
424
416
  */
425
417
  export interface UsernamePasswordConfiguration extends AuthenticationConfiguration {
426
418
  strategy: 'UsernamePassword'
427
- /**
428
- * The specific property within the User entity that holds the password.
429
- * This property must be marked with the "Password" data semantic in the Data Modeler.
430
- *
431
- * This property is required to publish the API.
432
- */
433
- passwordKey?: string
434
419
  }
435
420
 
436
421
  export type AuthenticationStrategy = UsernamePasswordConfiguration
@@ -897,3 +882,59 @@ export interface ApiModelValidationResult {
897
882
  */
898
883
  issues: ApiModelValidationItem[]
899
884
  }
885
+
886
+ /**
887
+ * Schema definition for an association.
888
+ *
889
+ * This is used to define how an association is represented in the schema.
890
+ */
891
+ export interface AssociationSchema {
892
+ /**
893
+ * Whether the target entity should be embedded under the property name.
894
+ * When false, this association is just an information that one entity depend on another.
895
+ * When true, it changes the definition of the schema having this association to
896
+ * add the target schema properties inline with this property.
897
+ *
898
+ * **When true**
899
+ *
900
+ * ```javascript
901
+ * // generated schema for `address` association
902
+ * {
903
+ * "name": "example value",
904
+ * "address": {
905
+ * "city": "example value",
906
+ * ...
907
+ * }
908
+ * }
909
+ * ```
910
+ *
911
+ * **When false**
912
+ *
913
+ * ```javascript
914
+ * // generated schema for `address` association
915
+ * {
916
+ * "name": "example value",
917
+ * "address": "the key of the referenced schema"
918
+ * }
919
+ * ```
920
+ */
921
+ embedded?: boolean
922
+ /**
923
+ * When the association has multiple targets the union type should be
924
+ * set to describe which union this is.
925
+ *
926
+ * Possible values are:
927
+ *
928
+ * - allOf - To validate against `allOf`, the given data must be valid against
929
+ * all of the given sub-schemas. When generating, it's a sum of all properties.
930
+ * - anyOf - To validate against `anyOf`, the given data must be valid against
931
+ * any (one or more) of the given sub-schemas. When generation a schema, it takes first union schema.
932
+ * - oneOf - To validate against `oneOf`, the given data must be valid against
933
+ * exactly one of the given sub-schemas. It behaves the same as `anyOf` when generating a schema
934
+ * - not - The `not` keyword declares that an instance validates if it doesn’t
935
+ * validate against the given sub-schema. It has no use when generating a schema.
936
+ *
937
+ * @default anyOf
938
+ */
939
+ unionType?: 'allOf' | 'anyOf' | 'oneOf' | 'not'
940
+ }
@@ -5,10 +5,10 @@ import { ListAction } from '../actions/ListAction.js'
5
5
  import { DeleteAction } from '../actions/DeleteAction.js'
6
6
  import { UpdateAction } from '../actions/UpdateAction.js'
7
7
  import { SearchAction } from '../actions/SearchAction.js'
8
- import type { RolesBasedAccessControl, UsernamePasswordConfiguration } from '../types.js'
9
8
  import type { ApiModelValidationItem, ApiModelValidationContext } from '../types.js'
10
9
  import { ApiModelKind, ExposedEntityKind } from '../../models/kinds.js'
11
10
  import { SemanticType } from '../Semantics.js'
11
+ import { DomainProperty } from '../DomainProperty.js'
12
12
 
13
13
  /**
14
14
  * Creates a unique validation code.
@@ -127,35 +127,32 @@ export function validateApiModelSecurity(model: ApiModel): ApiModelValidationIte
127
127
  context: { ...context, property: 'authentication' },
128
128
  })
129
129
  } else if (model.authentication.strategy === 'UsernamePassword') {
130
- const auth = model.authentication as UsernamePasswordConfiguration
131
- if (!auth.passwordKey) {
132
- issues.push({
133
- code: createCode('API', 'MISSING_PASSWORD_KEY'),
134
- message: 'Username & Password authentication requires a specific field for the password.',
135
- suggestion:
136
- 'Select which field in your user profile should store the password. ' +
137
- 'The data domain model should have a password data semantic on that property.',
138
- severity: 'error',
139
- context: { ...context, property: 'authentication.passwordKey' },
140
- })
141
- } else if (userEntity) {
142
- const passwordProp = Array.from(userEntity.properties).find((p) => p.key === auth.passwordKey)
143
- if (passwordProp && !passwordProp.hasSemantic(SemanticType.Password)) {
130
+ if (userEntity) {
131
+ let passwordProp: DomainProperty | undefined
132
+ for (const prop of userEntity.properties) {
133
+ if (prop.hasSemantic(SemanticType.Password)) {
134
+ passwordProp = prop
135
+ break
136
+ }
137
+ }
138
+ if (!passwordProp) {
144
139
  issues.push({
145
140
  code: createCode('API', 'MISSING_PASSWORD_SEMANTIC'),
146
- message: 'The selected password field is missing the Password data semantic.',
147
- suggestion: 'Go to the Data Modeler and add the "Password" semantic to this property.',
141
+ message: 'The selected user model requires a property with the Password data semantic for authentication.',
142
+ suggestion: 'Go to the Data Modeler and add the "Password" semantic to the password property.',
148
143
  severity: 'error',
149
- context: { ...context, property: 'authentication.passwordKey' },
144
+ context: { ...context, property: 'user' },
150
145
  })
151
146
  }
152
- }
153
147
 
154
- if (userEntity) {
155
- const hasUsernameSemantic = Array.from(userEntity.properties).some(
156
- (p) => typeof p.hasSemantic === 'function' && p.hasSemantic(SemanticType.Username)
157
- )
158
- if (!hasUsernameSemantic) {
148
+ let usernameProp: DomainProperty | undefined
149
+ for (const prop of userEntity.properties) {
150
+ if (prop.hasSemantic(SemanticType.Username)) {
151
+ usernameProp = prop
152
+ break
153
+ }
154
+ }
155
+ if (!usernameProp) {
159
156
  issues.push({
160
157
  code: createCode('API', 'MISSING_USERNAME_SEMANTIC'),
161
158
  message: 'Username & Password authentication requires a field with the Username data semantic.',
@@ -177,24 +174,21 @@ export function validateApiModelSecurity(model: ApiModel): ApiModelValidationIte
177
174
  context: { ...context, property: 'authorization' },
178
175
  })
179
176
  } else if (model.authorization.strategy === 'RBAC') {
180
- const rbac = model.authorization as RolesBasedAccessControl
181
- if (!rbac.roleKey) {
182
- issues.push({
183
- code: createCode('API', 'MISSING_ROLE_KEY'),
184
- message: 'Role-based access control is selected but no role field has been defined.',
185
- suggestion: "Select which field in your user profile determines the user's role.",
186
- severity: 'error',
187
- context: { ...context, property: 'authorization.roleKey' },
188
- })
189
- } else if (userEntity) {
190
- const roleProp = Array.from(userEntity.properties).find((p) => p.key === rbac.roleKey)
191
- if (roleProp && !roleProp.hasSemantic(SemanticType.UserRole)) {
177
+ if (userEntity) {
178
+ let roleProp: DomainProperty | undefined
179
+ for (const prop of userEntity.properties) {
180
+ if (prop.hasSemantic(SemanticType.UserRole)) {
181
+ roleProp = prop
182
+ break
183
+ }
184
+ }
185
+ if (!roleProp) {
192
186
  issues.push({
193
187
  code: createCode('API', 'MISSING_ROLE_SEMANTIC'),
194
- message: 'The selected role field is missing the User Role data semantic.',
195
- suggestion: 'Go to the Data Modeler and add the "User Role" semantic to this property.',
188
+ message: 'Role-based access control requires a property with the User Role data semantic on the user model.',
189
+ suggestion: 'Go to the Data Modeler and add the "User Role" semantic to the property used for roles.',
196
190
  severity: 'error',
197
- context: { ...context, property: 'authorization.roleKey' },
191
+ context: { ...context, property: 'user' },
198
192
  })
199
193
  }
200
194
  }
@@ -229,15 +223,23 @@ export function validateApiModelSecurity(model: ApiModel): ApiModelValidationIte
229
223
  context: { ...context, property: 'session.properties' },
230
224
  })
231
225
  } else if (model.authorization && model.authorization.strategy === 'RBAC') {
232
- const rbac = model.authorization as RolesBasedAccessControl
233
- if (rbac.roleKey && !model.session.properties.includes(rbac.roleKey)) {
234
- issues.push({
235
- code: createCode('API', 'MISSING_RBAC_SESSION_PROPERTY'),
236
- message: 'The user role must be included in the session data for permissions to work.',
237
- suggestion: 'Make sure your selected role field is checked in the session settings.',
238
- severity: 'error',
239
- context: { ...context, property: 'session.properties' },
240
- })
226
+ if (userEntity) {
227
+ let roleProp: DomainProperty | undefined
228
+ for (const prop of userEntity.properties) {
229
+ if (prop.hasSemantic(SemanticType.UserRole)) {
230
+ roleProp = prop
231
+ break
232
+ }
233
+ }
234
+ if (roleProp && !model.session.properties.includes(roleProp.key)) {
235
+ issues.push({
236
+ code: createCode('API', 'MISSING_RBAC_SESSION_PROPERTY'),
237
+ message: 'The user role must be included in the session data for permissions to work.',
238
+ suggestion: 'Make sure your selected role property is checked in the session settings.',
239
+ severity: 'error',
240
+ context: { ...context, property: 'session.properties' },
241
+ })
242
+ }
241
243
  }
242
244
  }
243
245
 
@@ -23,9 +23,9 @@ Validations are evaluated with one of three severity levels:
23
23
  ## 3. Security & Access Control
24
24
 
25
25
  - **Authentication** [Error]: The `authentication` configuration is required.
26
- - If the strategy is `UsernamePassword`, the `passwordKey` must be defined.
26
+ - If the strategy is `UsernamePassword`, the user entity must contain a property with the `Username` semantic.
27
27
  - **Authorization** [Error]: The `authorization` configuration is required.
28
- - If the strategy is `RBAC`, the `roleKey` must be defined.
28
+ - If the strategy is `RBAC`, the user entity must contain a property with the `UserRole` semantic.
29
29
  - **Session Configuration** [Error]: The `session` configuration is required and must meet the following criteria:
30
30
  - **Secret**: A session encryption token (`secret`) is required.
31
31
  - **Properties**: The `session.properties` array must have at least one property set (e.g. to identify the User ID).
@@ -2,6 +2,7 @@ import { test } from '@japa/runner'
2
2
  import { ApiModel } from '../../../src/modeling/ApiModel.js'
3
3
  import { RuntimeApiModel, type RuntimeApiModelSchema } from '../../../src/modeling/RuntimeApiModel.js'
4
4
  import { DataDomain } from '../../../src/modeling/DataDomain.js'
5
+ import { SemanticType } from '../../../src/modeling/Semantics.js'
5
6
 
6
7
  test.group('RuntimeApiModel', () => {
7
8
  test('initializes from schema', ({ assert }) => {
@@ -119,4 +120,34 @@ test.group('RuntimeApiModel', () => {
119
120
  const missingActionResult = runtimeModel.lookupAction('GET', '/missing-action')
120
121
  assert.isUndefined(missingActionResult)
121
122
  }).tags(['@modeling', '@runtime'])
123
+
124
+ test('caches user entity and properties based on semantics', ({ assert }) => {
125
+ const domain = new DataDomain({ info: { version: '1.0.0' } })
126
+ const modelNode = domain.addModel({ key: 'users' })
127
+ const userEntity = modelNode.addEntity({ info: { name: 'User' }, semantics: [{ id: SemanticType.User }] })
128
+
129
+ const passwordProp = userEntity.addProperty({
130
+ info: { name: 'Password' },
131
+ semantics: [{ id: SemanticType.Password }],
132
+ })
133
+ const roleProp = userEntity.addProperty({ info: { name: 'Role' }, semantics: [{ id: SemanticType.UserRole }] })
134
+ const usernameProp = userEntity.addProperty({
135
+ info: { name: 'Username' },
136
+ semantics: [{ id: SemanticType.Username }],
137
+ })
138
+
139
+ const schema = {
140
+ key: 'api-1',
141
+ info: { name: 'Test API' },
142
+ user: { key: userEntity.key, domain: domain.key },
143
+ routingMap: {},
144
+ }
145
+
146
+ const runtimeModel = new RuntimeApiModel(schema as any, domain.toJSON())
147
+
148
+ assert.equal(runtimeModel.cachedEntities.user?.key, userEntity.key)
149
+ assert.equal(runtimeModel.cachedProperties.password?.key, passwordProp.key)
150
+ assert.equal(runtimeModel.cachedProperties.role?.key, roleProp.key)
151
+ assert.equal(runtimeModel.cachedProperties.username?.key, usernameProp.key)
152
+ }).tags(['@modeling', '@runtime'])
122
153
  })