@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.
- package/build/src/modeling/ApiModel.d.ts.map +1 -1
- package/build/src/modeling/ApiModel.js +0 -8
- package/build/src/modeling/ApiModel.js.map +1 -1
- package/build/src/modeling/DomainAssociation.d.ts +4 -4
- package/build/src/modeling/DomainAssociation.d.ts.map +1 -1
- package/build/src/modeling/DomainAssociation.js.map +1 -1
- package/build/src/modeling/RuntimeApiModel.d.ts +16 -0
- package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -1
- package/build/src/modeling/RuntimeApiModel.js +31 -0
- package/build/src/modeling/RuntimeApiModel.js.map +1 -1
- package/build/src/modeling/ai/domain_response_schema.d.ts +4 -2
- package/build/src/modeling/ai/domain_response_schema.d.ts.map +1 -1
- package/build/src/modeling/ai/domain_response_schema.js +1 -1
- package/build/src/modeling/ai/domain_response_schema.js.map +1 -1
- package/build/src/modeling/ai/types.d.ts +2 -3
- package/build/src/modeling/ai/types.d.ts.map +1 -1
- package/build/src/modeling/ai/types.js.map +1 -1
- package/build/src/modeling/amf/ShapeGenerator.js +1 -1
- package/build/src/modeling/amf/ShapeGenerator.js.map +1 -1
- package/build/src/modeling/generators/oas_312/OasGenerator.js +1 -1
- package/build/src/modeling/generators/oas_312/OasGenerator.js.map +1 -1
- package/build/src/modeling/generators/oas_312/OasSchemaGenerator.js +3 -3
- package/build/src/modeling/generators/oas_312/OasSchemaGenerator.js.map +1 -1
- package/build/src/modeling/generators/oas_320/OasSchemaGenerator.js +3 -3
- package/build/src/modeling/generators/oas_320/OasSchemaGenerator.js.map +1 -1
- package/build/src/modeling/types.d.ts +55 -15
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -1
- package/build/src/modeling/validation/api_model_rules.js +49 -46
- package/build/src/modeling/validation/api_model_rules.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/modeling/ApiModel.ts +0 -10
- package/src/modeling/DomainAssociation.ts +4 -4
- package/src/modeling/RuntimeApiModel.ts +46 -0
- package/src/modeling/ai/domain_response_schema.ts +1 -1
- package/src/modeling/ai/types.ts +2 -3
- package/src/modeling/amf/ShapeGenerator.ts +1 -1
- package/src/modeling/generators/oas_312/OasGenerator.ts +1 -1
- package/src/modeling/generators/oas_312/OasSchemaGenerator.ts +3 -3
- package/src/modeling/generators/oas_320/OasSchemaGenerator.ts +3 -3
- package/src/modeling/readme.md +2 -2
- package/src/modeling/types.ts +56 -15
- package/src/modeling/validation/api_model_rules.ts +50 -48
- package/src/modeling/validation/api_model_validation_rules.md +2 -2
- package/tests/unit/modeling/RuntimeApiModel.spec.ts +31 -0
- package/tests/unit/modeling/amf/shape_generator.spec.ts +32 -14
- package/tests/unit/modeling/api_model.spec.ts +6 -6
- package/tests/unit/modeling/data_domain_change_observers.spec.ts +1 -1
- package/tests/unit/modeling/domain_asociation.spec.ts +18 -18
- package/tests/unit/modeling/domain_entity_example_generator_json.spec.ts +68 -23
- package/tests/unit/modeling/domain_entity_example_generator_xml.spec.ts +32 -9
- package/tests/unit/modeling/generators/OasGenerator.spec.ts +3 -5
- package/tests/unit/modeling/validation/api_model_rules.spec.ts +12 -12
package/package.json
CHANGED
package/src/modeling/ApiModel.ts
CHANGED
|
@@ -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?:
|
|
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:
|
|
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():
|
|
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
|
-
|
|
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
|
}
|
package/src/modeling/ai/types.ts
CHANGED
|
@@ -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?:
|
|
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
|
|
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?.
|
|
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?.
|
|
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
|
|
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
|
|
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?.
|
|
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
|
|
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
|
|
94
|
+
// If "embedded", add the resource as a reference to the schema.
|
|
95
95
|
if (targetEntity !== entity) {
|
|
96
96
|
this.generateEntity(targetEntity)
|
|
97
97
|
}
|
package/src/modeling/readme.md
CHANGED
|
@@ -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'
|
|
191
|
-
apiModel.authorization = { strategy: 'RBAC'
|
|
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'],
|
package/src/modeling/types.ts
CHANGED
|
@@ -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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
147
|
-
suggestion: 'Go to the Data Modeler and add the "Password" semantic to
|
|
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: '
|
|
144
|
+
context: { ...context, property: 'user' },
|
|
150
145
|
})
|
|
151
146
|
}
|
|
152
|
-
}
|
|
153
147
|
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
(
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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: '
|
|
195
|
-
suggestion: 'Go to the Data Modeler and add the "User Role" semantic to
|
|
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: '
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
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
|
|
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
|
})
|