@api-client/core 0.19.11 → 0.19.13
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/browser.d.ts +3 -1
- package/build/src/browser.d.ts.map +1 -1
- package/build/src/browser.js +2 -0
- package/build/src/browser.js.map +1 -1
- package/build/src/index.d.ts +3 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +2 -0
- package/build/src/index.js.map +1 -1
- package/build/src/modeling/ApiValidation.d.ts +26 -0
- package/build/src/modeling/ApiValidation.d.ts.map +1 -0
- package/build/src/modeling/ApiValidation.js +73 -0
- package/build/src/modeling/ApiValidation.js.map +1 -0
- package/build/src/modeling/DomainValidation.d.ts +10 -4
- package/build/src/modeling/DomainValidation.d.ts.map +1 -1
- package/build/src/modeling/DomainValidation.js +55 -72
- package/build/src/modeling/DomainValidation.js.map +1 -1
- package/build/src/modeling/ExposedEntity.js +4 -4
- package/build/src/modeling/ExposedEntity.js.map +1 -1
- package/build/src/modeling/validation/api_model_rules.d.ts +1 -2
- package/build/src/modeling/validation/api_model_rules.d.ts.map +1 -1
- package/build/src/modeling/validation/api_model_rules.js +3 -29
- package/build/src/modeling/validation/api_model_rules.js.map +1 -1
- package/build/src/modeling/validation/association_validation.d.ts +4 -4
- package/build/src/modeling/validation/association_validation.d.ts.map +1 -1
- package/build/src/modeling/validation/association_validation.js.map +1 -1
- package/build/src/modeling/validation/entity_validation.d.ts +6 -6
- package/build/src/modeling/validation/entity_validation.d.ts.map +1 -1
- package/build/src/modeling/validation/entity_validation.js.map +1 -1
- package/build/src/modeling/validation/property_validation.d.ts +3 -3
- package/build/src/modeling/validation/property_validation.d.ts.map +1 -1
- package/build/src/modeling/validation/property_validation.js.map +1 -1
- package/build/src/modeling/validation/rules.d.ts +2 -2
- package/build/src/modeling/validation/rules.d.ts.map +1 -1
- package/build/src/modeling/validation/rules.js.map +1 -1
- package/build/src/modeling/validation/semantic_validation.d.ts +2 -2
- package/build/src/modeling/validation/semantic_validation.d.ts.map +1 -1
- package/build/src/modeling/validation/semantic_validation.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/modeling/ApiValidation.ts +86 -0
- package/src/modeling/DomainValidation.ts +57 -74
- package/src/modeling/ExposedEntity.ts +4 -4
- package/src/modeling/validation/api_model_rules.ts +4 -34
- package/src/modeling/validation/association_validation.ts +6 -6
- package/src/modeling/validation/entity_validation.ts +11 -11
- package/src/modeling/validation/property_validation.ts +4 -4
- package/src/modeling/validation/rules.ts +6 -3
- package/src/modeling/validation/semantic_validation.ts +11 -11
- package/tests/unit/modeling/domain_validation.spec.ts +7 -13
- package/tests/unit/modeling/exposed_entity.spec.ts +5 -5
- package/tests/unit/modeling/exposed_entity_setter_validation.spec.ts +5 -5
- package/tests/unit/modeling/validation/api_model_rules.spec.ts +24 -9
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.
|
|
4
|
+
"version": "0.19.13",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./browser.js": {
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"chalk": "^5.4.1",
|
|
97
97
|
"console-table-printer": "^2.11.2",
|
|
98
98
|
"dompurify": "^3.2.6",
|
|
99
|
-
"jsdom": "^
|
|
99
|
+
"jsdom": "^29.0.0",
|
|
100
100
|
"nanoid": "^5.1.5",
|
|
101
101
|
"tslog": "^4.9.3",
|
|
102
102
|
"ws": "^8.12.0",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ApiModel } from './ApiModel.js'
|
|
2
|
+
import { ApiModelValidationItem } from './types.js'
|
|
3
|
+
import { ApiModelKind } from '../models/kinds.js'
|
|
4
|
+
import {
|
|
5
|
+
validateApiModelInfo,
|
|
6
|
+
validateApiModelDependency,
|
|
7
|
+
validateApiModelSecurity,
|
|
8
|
+
validateApiModelMetadata,
|
|
9
|
+
validateExposedEntity,
|
|
10
|
+
} from './validation/api_model_rules.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ApiValidation performs comprehensive validation on an API model.
|
|
14
|
+
*
|
|
15
|
+
* This class orchestrates validation across all API model elements including
|
|
16
|
+
* info, security, exposures, and actions. It ensures that the API model is
|
|
17
|
+
* well-formed and follows established conventions before it can be published.
|
|
18
|
+
*/
|
|
19
|
+
export class ApiValidation {
|
|
20
|
+
report: ApiModelValidationItem[] = []
|
|
21
|
+
#infoCount = 0
|
|
22
|
+
#warningCount = 0
|
|
23
|
+
#errorCount = 0
|
|
24
|
+
#counted = false
|
|
25
|
+
|
|
26
|
+
get infoCount(): number {
|
|
27
|
+
if (!this.#counted) {
|
|
28
|
+
this.computeIssueCounters()
|
|
29
|
+
}
|
|
30
|
+
return this.#infoCount
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get warningCount(): number {
|
|
34
|
+
if (!this.#counted) {
|
|
35
|
+
this.computeIssueCounters()
|
|
36
|
+
}
|
|
37
|
+
return this.#warningCount
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get errorCount(): number {
|
|
41
|
+
if (!this.#counted) {
|
|
42
|
+
this.computeIssueCounters()
|
|
43
|
+
}
|
|
44
|
+
return this.#errorCount
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
constructor(protected model: ApiModel) {}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Performs comprehensive validation on the entire API model.
|
|
51
|
+
*
|
|
52
|
+
* @returns A comprehensive validation report with all issues found
|
|
53
|
+
*/
|
|
54
|
+
validate(): ApiModelValidationItem[] {
|
|
55
|
+
this.#counted = false
|
|
56
|
+
this.report = []
|
|
57
|
+
|
|
58
|
+
this.report.push(...validateApiModelInfo(this.model))
|
|
59
|
+
this.report.push(...validateApiModelDependency(this.model))
|
|
60
|
+
this.report.push(...validateApiModelSecurity(this.model))
|
|
61
|
+
this.report.push(...validateApiModelMetadata(this.model))
|
|
62
|
+
|
|
63
|
+
if (!this.model.exposes || this.model.exposes.size === 0) {
|
|
64
|
+
this.report.push({
|
|
65
|
+
code: 'API_NO_EXPOSURES',
|
|
66
|
+
message: 'Your API currently has no exposed data for external clients to request.',
|
|
67
|
+
suggestion: 'Expose a data model so apps can communicate with it.',
|
|
68
|
+
severity: 'warning',
|
|
69
|
+
context: { apiModelKey: this.model.key, kind: ApiModelKind, key: this.model.key, property: 'exposes' },
|
|
70
|
+
})
|
|
71
|
+
} else {
|
|
72
|
+
for (const exposure of this.model.exposes.values()) {
|
|
73
|
+
this.report.push(...validateExposedEntity(exposure, this.model))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return this.report
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
computeIssueCounters(): void {
|
|
81
|
+
this.#infoCount = this.report.filter((validation) => validation.severity === 'info').length
|
|
82
|
+
this.#warningCount = this.report.filter((validation) => validation.severity === 'warning').length
|
|
83
|
+
this.#errorCount = this.report.filter((validation) => validation.severity === 'error').length
|
|
84
|
+
this.#counted = true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { DataDomainKind } from '../models/kinds.js'
|
|
2
1
|
import { DataDomain } from './DataDomain.js'
|
|
3
|
-
import type { DomainImpactReport } from './types.js'
|
|
4
2
|
import { AssociationValidation } from './validation/association_validation.js'
|
|
5
3
|
import { EntityValidation } from './validation/entity_validation.js'
|
|
6
4
|
import { PropertyValidation } from './validation/property_validation.js'
|
|
5
|
+
import { DomainValidationSchema } from './validation/rules.js'
|
|
7
6
|
import { SemanticValidation } from './validation/semantic_validation.js'
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -76,12 +75,35 @@ import { SemanticValidation } from './validation/semantic_validation.js'
|
|
|
76
75
|
* @see {@link SemanticValidation} for detailed semantic validation rules
|
|
77
76
|
*/
|
|
78
77
|
export class DomainValidation {
|
|
79
|
-
|
|
78
|
+
report: DomainValidationSchema[] = []
|
|
79
|
+
#infoCount = 0
|
|
80
|
+
#warningCount = 0
|
|
81
|
+
#errorCount = 0
|
|
82
|
+
#counted = false
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
this
|
|
84
|
+
get infoCount(): number {
|
|
85
|
+
if (!this.#counted) {
|
|
86
|
+
this.computeIssueCounters()
|
|
87
|
+
}
|
|
88
|
+
return this.#infoCount
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get warningCount(): number {
|
|
92
|
+
if (!this.#counted) {
|
|
93
|
+
this.computeIssueCounters()
|
|
94
|
+
}
|
|
95
|
+
return this.#warningCount
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get errorCount(): number {
|
|
99
|
+
if (!this.#counted) {
|
|
100
|
+
this.computeIssueCounters()
|
|
101
|
+
}
|
|
102
|
+
return this.#errorCount
|
|
83
103
|
}
|
|
84
104
|
|
|
105
|
+
constructor(protected domain: DataDomain) {}
|
|
106
|
+
|
|
85
107
|
/**
|
|
86
108
|
* Performs comprehensive validation on the entire data domain.
|
|
87
109
|
*
|
|
@@ -91,93 +113,54 @@ export class DomainValidation {
|
|
|
91
113
|
*
|
|
92
114
|
* @returns A comprehensive validation report with all issues found
|
|
93
115
|
*/
|
|
94
|
-
validate():
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const entityValidator = new EntityValidation(this.root)
|
|
102
|
-
const propertyValidator = new PropertyValidation(this.root)
|
|
103
|
-
const associationValidator = new AssociationValidation(this.root)
|
|
104
|
-
const semanticValidator = new SemanticValidation(this.root)
|
|
116
|
+
validate(): DomainValidationSchema[] {
|
|
117
|
+
this.#counted = false
|
|
118
|
+
this.report = []
|
|
119
|
+
const entityValidator = new EntityValidation(this.domain)
|
|
120
|
+
const propertyValidator = new PropertyValidation(this.domain)
|
|
121
|
+
const associationValidator = new AssociationValidation(this.domain)
|
|
122
|
+
const semanticValidator = new SemanticValidation(this.domain)
|
|
105
123
|
|
|
106
124
|
let hasEntities = false
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (entity.domain.key !== this.root.key) {
|
|
125
|
+
for (const entity of this.domain.listEntities()) {
|
|
126
|
+
if (entity.domain.key !== this.domain.key) {
|
|
110
127
|
// we don't need to validate foreign entities
|
|
111
128
|
continue
|
|
112
129
|
}
|
|
113
130
|
hasEntities = true
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const blocking = item.severity === 'error'
|
|
117
|
-
result.canProceed = result.canProceed && !blocking
|
|
118
|
-
result.impact.push({
|
|
119
|
-
key: item.key,
|
|
120
|
-
kind: item.kind,
|
|
121
|
-
type: 'publish',
|
|
122
|
-
impact: item.message,
|
|
123
|
-
resolution: item.help,
|
|
124
|
-
severity: item.severity,
|
|
125
|
-
parent: item.parent,
|
|
126
|
-
})
|
|
127
|
-
}
|
|
131
|
+
const entities = entityValidator.validate(entity)
|
|
132
|
+
this.report.push(...entities)
|
|
128
133
|
for (const property of entity.properties) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
result.canProceed = result.canProceed && !blocking
|
|
133
|
-
result.impact.push({
|
|
134
|
-
key: item.key,
|
|
135
|
-
kind: item.kind,
|
|
136
|
-
type: 'publish',
|
|
137
|
-
impact: item.message,
|
|
138
|
-
resolution: item.help,
|
|
139
|
-
severity: item.severity,
|
|
140
|
-
parent: item.parent,
|
|
141
|
-
})
|
|
134
|
+
if (property.domain.key !== this.domain.key) {
|
|
135
|
+
// we don't need to validate foreign properties
|
|
136
|
+
continue
|
|
142
137
|
}
|
|
138
|
+
const properties = propertyValidator.validate(property)
|
|
139
|
+
this.report.push(...properties)
|
|
143
140
|
}
|
|
144
141
|
for (const association of entity.associations) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
result.canProceed = result.canProceed && !blocking
|
|
149
|
-
result.impact.push({
|
|
150
|
-
key: item.key,
|
|
151
|
-
kind: item.kind,
|
|
152
|
-
type: 'publish',
|
|
153
|
-
impact: item.message,
|
|
154
|
-
resolution: item.help,
|
|
155
|
-
severity: item.severity,
|
|
156
|
-
parent: item.parent,
|
|
157
|
-
})
|
|
142
|
+
if (association.domain.key !== this.domain.key) {
|
|
143
|
+
// we don't need to validate foreign properties
|
|
144
|
+
continue
|
|
158
145
|
}
|
|
146
|
+
const associations = associationValidator.validate(association)
|
|
147
|
+
this.report.push(...associations)
|
|
159
148
|
}
|
|
160
149
|
}
|
|
161
150
|
if (!hasEntities) {
|
|
162
151
|
// no entities, no need to validate anything else
|
|
163
|
-
return
|
|
152
|
+
return this.report
|
|
164
153
|
}
|
|
165
154
|
// Validate semantics
|
|
166
155
|
const semanticReport = semanticValidator.validate()
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
result.impact.push({
|
|
171
|
-
key: item.key,
|
|
172
|
-
kind: item.kind,
|
|
173
|
-
type: 'publish',
|
|
174
|
-
impact: item.message,
|
|
175
|
-
resolution: item.help,
|
|
176
|
-
severity: item.severity,
|
|
177
|
-
parent: item.parent,
|
|
178
|
-
})
|
|
179
|
-
}
|
|
156
|
+
this.report.push(...semanticReport)
|
|
157
|
+
return this.report
|
|
158
|
+
}
|
|
180
159
|
|
|
181
|
-
|
|
160
|
+
computeIssueCounters(): void {
|
|
161
|
+
this.#infoCount = this.report.filter((validation) => validation.severity === 'info').length
|
|
162
|
+
this.#warningCount = this.report.filter((validation) => validation.severity === 'warning').length
|
|
163
|
+
this.#errorCount = this.report.filter((validation) => validation.severity === 'error').length
|
|
164
|
+
this.#counted = true
|
|
182
165
|
}
|
|
183
166
|
}
|
|
@@ -318,10 +318,10 @@ export class ExposedEntity extends EventTarget {
|
|
|
318
318
|
return
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
// No collection: allow
|
|
322
|
-
if (segments.length !==
|
|
321
|
+
// No collection: allow exactly one segment
|
|
322
|
+
if (segments.length !== 1) {
|
|
323
323
|
throw new Error(
|
|
324
|
-
`Resource path must contain exactly
|
|
324
|
+
`Resource path must contain exactly one segment when no collection is present. Received: "${cleaned}"`
|
|
325
325
|
)
|
|
326
326
|
}
|
|
327
327
|
// Check for collision if this is a root entity (singleton case)
|
|
@@ -333,7 +333,7 @@ export class ExposedEntity extends EventTarget {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
if (this.resourcePath !== cleaned) {
|
|
336
|
-
this.resourcePath = `/${segments[0]}
|
|
336
|
+
this.resourcePath = `/${segments[0]}`
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
|
|
@@ -6,7 +6,7 @@ import { DeleteAction } from '../actions/DeleteAction.js'
|
|
|
6
6
|
import { UpdateAction } from '../actions/UpdateAction.js'
|
|
7
7
|
import { SearchAction } from '../actions/SearchAction.js'
|
|
8
8
|
import type { RolesBasedAccessControl, UsernamePasswordConfiguration } from '../types.js'
|
|
9
|
-
import type { ApiModelValidationItem,
|
|
9
|
+
import type { ApiModelValidationItem, ApiModelValidationContext } from '../types.js'
|
|
10
10
|
import { ApiModelKind, ExposedEntityKind } from '../../models/kinds.js'
|
|
11
11
|
import { SemanticType } from '../Semantics.js'
|
|
12
12
|
|
|
@@ -522,11 +522,11 @@ export function validateExposedEntity(entity: ExposedEntity, apiModel: ApiModel)
|
|
|
522
522
|
})
|
|
523
523
|
} else {
|
|
524
524
|
const parts = entity.resourcePath.split('/').filter(Boolean)
|
|
525
|
-
if (parts.length !==
|
|
525
|
+
if (parts.length !== 1 || !entity.resourcePath.startsWith('/')) {
|
|
526
526
|
issues.push({
|
|
527
527
|
code: createCode('EXPOSURE', 'INVALID_RESOURCE_PATH_FORMAT'),
|
|
528
|
-
message: 'The URL route must only contain
|
|
529
|
-
suggestion: 'Simplify the endpoint URL to
|
|
528
|
+
message: 'The URL route must only contain one exact part or level.',
|
|
529
|
+
suggestion: 'Simplify the endpoint URL to a single segment.',
|
|
530
530
|
severity: 'error',
|
|
531
531
|
context: { ...context, property: 'resourcePath' },
|
|
532
532
|
})
|
|
@@ -608,33 +608,3 @@ export function validateExposedEntity(entity: ExposedEntity, apiModel: ApiModel)
|
|
|
608
608
|
|
|
609
609
|
return issues
|
|
610
610
|
}
|
|
611
|
-
|
|
612
|
-
export function validateApiModel(model: ApiModel): ApiModelValidationResult {
|
|
613
|
-
const issues: ApiModelValidationItem[] = []
|
|
614
|
-
|
|
615
|
-
issues.push(...validateApiModelInfo(model))
|
|
616
|
-
issues.push(...validateApiModelDependency(model))
|
|
617
|
-
issues.push(...validateApiModelSecurity(model))
|
|
618
|
-
issues.push(...validateApiModelMetadata(model))
|
|
619
|
-
|
|
620
|
-
if (!model.exposes || model.exposes.size === 0) {
|
|
621
|
-
issues.push({
|
|
622
|
-
code: createCode('API', 'NO_EXPOSURES'),
|
|
623
|
-
message: 'Your API currently has no exposed data for external clients to request.',
|
|
624
|
-
suggestion: 'Expose a data model so apps can communicate with it.',
|
|
625
|
-
severity: 'warning',
|
|
626
|
-
context: { apiModelKey: model.key, kind: ApiModelKind, key: model.key, property: 'exposes' },
|
|
627
|
-
})
|
|
628
|
-
} else {
|
|
629
|
-
for (const exposure of model.exposes.values()) {
|
|
630
|
-
issues.push(...validateExposedEntity(exposure, model))
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const hasErrors = issues.some((i) => i.severity === 'error')
|
|
635
|
-
|
|
636
|
-
return {
|
|
637
|
-
isValid: !hasErrors,
|
|
638
|
-
issues,
|
|
639
|
-
}
|
|
640
|
-
}
|
|
@@ -2,7 +2,7 @@ import { DomainAssociationKind } from '../../models/kinds.js'
|
|
|
2
2
|
import type { DataDomain } from '../DataDomain.js'
|
|
3
3
|
import type { DomainAssociation } from '../DomainAssociation.js'
|
|
4
4
|
import type { DomainEntity } from '../DomainEntity.js'
|
|
5
|
-
import { type
|
|
5
|
+
import { type DomainValidationSchema, validatePropertyName } from './rules.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* AssociationValidation is a class that performs validation on associations in a data domain.
|
|
@@ -16,8 +16,8 @@ export class AssociationValidation {
|
|
|
16
16
|
* @param target The target association to validate. Can be a string with
|
|
17
17
|
* the association key or a DomainAssociation object.
|
|
18
18
|
*/
|
|
19
|
-
validate(target: string | DomainAssociation):
|
|
20
|
-
const results:
|
|
19
|
+
validate(target: string | DomainAssociation): DomainValidationSchema[] {
|
|
20
|
+
const results: DomainValidationSchema[] = []
|
|
21
21
|
let association: DomainAssociation | undefined
|
|
22
22
|
if (typeof target === 'string') {
|
|
23
23
|
association = this.domain.findAssociation(target)
|
|
@@ -58,7 +58,7 @@ export class AssociationValidation {
|
|
|
58
58
|
* - (recommendation) Column names should be in lower case.
|
|
59
59
|
* @param association The association to validate
|
|
60
60
|
*/
|
|
61
|
-
validateName(association: DomainAssociation):
|
|
61
|
+
validateName(association: DomainAssociation): DomainValidationSchema[] {
|
|
62
62
|
return validatePropertyName(association)
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -66,8 +66,8 @@ export class AssociationValidation {
|
|
|
66
66
|
* Validates the association targets.
|
|
67
67
|
* @param association The association to validate
|
|
68
68
|
*/
|
|
69
|
-
validateTargets(association: DomainAssociation):
|
|
70
|
-
const results:
|
|
69
|
+
validateTargets(association: DomainAssociation): DomainValidationSchema[] {
|
|
70
|
+
const results: DomainValidationSchema[] = []
|
|
71
71
|
const label = association.info.getLabel()
|
|
72
72
|
const parentEntity = association.getParentInstance() as DomainEntity
|
|
73
73
|
if (!association.targets.length) {
|
|
@@ -2,7 +2,7 @@ import { DomainEntityKind } from '../../models/kinds.js'
|
|
|
2
2
|
import type { DataDomain } from '../DataDomain.js'
|
|
3
3
|
import type { DomainEntity } from '../DomainEntity.js'
|
|
4
4
|
import { ReservedKeywords } from './postgresql.js'
|
|
5
|
-
import type {
|
|
5
|
+
import type { DomainValidationSchema } from './rules.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* EntityValidation is a class that performs validation on entities in a data domain.
|
|
@@ -20,8 +20,8 @@ export class EntityValidation {
|
|
|
20
20
|
* @param target The target entity to validate. Can be a string with
|
|
21
21
|
* the entity key or a DomainEntity object.
|
|
22
22
|
*/
|
|
23
|
-
validate(target: string | DomainEntity):
|
|
24
|
-
const results:
|
|
23
|
+
validate(target: string | DomainEntity): DomainValidationSchema[] {
|
|
24
|
+
const results: DomainValidationSchema[] = []
|
|
25
25
|
let entity: DomainEntity | undefined
|
|
26
26
|
if (typeof target === 'string') {
|
|
27
27
|
entity = this.domain.findEntity(target)
|
|
@@ -57,8 +57,8 @@ export class EntityValidation {
|
|
|
57
57
|
* Validates the entity primary key.
|
|
58
58
|
* @param entity The entity to validate
|
|
59
59
|
*/
|
|
60
|
-
validatePrimaryKey(entity: DomainEntity):
|
|
61
|
-
const results:
|
|
60
|
+
validatePrimaryKey(entity: DomainEntity): DomainValidationSchema[] {
|
|
61
|
+
const results: DomainValidationSchema[] = []
|
|
62
62
|
const primary = entity.primaryKey()
|
|
63
63
|
if (!primary) {
|
|
64
64
|
const message = `The "${entity.info.getLabel()}" entity has no identifier.`
|
|
@@ -124,8 +124,8 @@ export class EntityValidation {
|
|
|
124
124
|
* Checks if the entity has the minimum required properties.
|
|
125
125
|
* @param entity The entity to validate
|
|
126
126
|
*/
|
|
127
|
-
minimumRequiredProperties(entity: DomainEntity):
|
|
128
|
-
const results:
|
|
127
|
+
minimumRequiredProperties(entity: DomainEntity): DomainValidationSchema[] {
|
|
128
|
+
const results: DomainValidationSchema[] = []
|
|
129
129
|
|
|
130
130
|
// Check if entity has properties or associations through entire inheritance chain
|
|
131
131
|
const hasProperties = this.hasPropertiesInherited(entity)
|
|
@@ -161,8 +161,8 @@ export class EntityValidation {
|
|
|
161
161
|
* - Should not be a reserved word (for example: "IN", "SELECT", "FROM", etc.).
|
|
162
162
|
* @param entity The entity to validate
|
|
163
163
|
*/
|
|
164
|
-
validateName(entity: DomainEntity):
|
|
165
|
-
const results:
|
|
164
|
+
validateName(entity: DomainEntity): DomainValidationSchema[] {
|
|
165
|
+
const results: DomainValidationSchema[] = []
|
|
166
166
|
const label = entity.info.getLabel()
|
|
167
167
|
if (!entity.info.name) {
|
|
168
168
|
const message = `The "${label}" entity has no name.`
|
|
@@ -252,8 +252,8 @@ export class EntityValidation {
|
|
|
252
252
|
* Checks if the entity name is unique in the data domain.
|
|
253
253
|
* @param entity The entity to validate
|
|
254
254
|
*/
|
|
255
|
-
uniqueName(entity: DomainEntity):
|
|
256
|
-
const results:
|
|
255
|
+
uniqueName(entity: DomainEntity): DomainValidationSchema[] {
|
|
256
|
+
const results: DomainValidationSchema[] = []
|
|
257
257
|
const name = entity.info.name?.toLowerCase()
|
|
258
258
|
if (!name) {
|
|
259
259
|
return results
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DomainPropertyKind } from '../../models/kinds.js'
|
|
2
2
|
import type { DataDomain } from '../DataDomain.js'
|
|
3
3
|
import type { DomainProperty } from '../DomainProperty.js'
|
|
4
|
-
import { type
|
|
4
|
+
import { type DomainValidationSchema, validatePropertyName } from './rules.js'
|
|
5
5
|
|
|
6
6
|
export class PropertyValidation {
|
|
7
7
|
constructor(protected domain: DataDomain) {}
|
|
@@ -12,8 +12,8 @@ export class PropertyValidation {
|
|
|
12
12
|
* @param target The target property to validate. Can be a string with
|
|
13
13
|
* the property key or a DomainProperty object.
|
|
14
14
|
*/
|
|
15
|
-
validate(target: string | DomainProperty):
|
|
16
|
-
const results:
|
|
15
|
+
validate(target: string | DomainProperty): DomainValidationSchema[] {
|
|
16
|
+
const results: DomainValidationSchema[] = []
|
|
17
17
|
let property: DomainProperty | undefined
|
|
18
18
|
if (typeof target === 'string') {
|
|
19
19
|
property = this.domain.findProperty(target)
|
|
@@ -52,7 +52,7 @@ export class PropertyValidation {
|
|
|
52
52
|
* - (recommendation) Column names should be in lower case.
|
|
53
53
|
* @param property The property to validate
|
|
54
54
|
*/
|
|
55
|
-
validateName(property: DomainProperty):
|
|
55
|
+
validateName(property: DomainProperty): DomainValidationSchema[] {
|
|
56
56
|
return validatePropertyName(property)
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -12,7 +12,10 @@ import { DomainPropertyKind } from '../../models/kinds.js'
|
|
|
12
12
|
* - `type` -> unused
|
|
13
13
|
* - `relationship` -> unused
|
|
14
14
|
*/
|
|
15
|
-
export interface
|
|
15
|
+
export interface DomainValidationSchema extends Omit<
|
|
16
|
+
DomainImpactItem,
|
|
17
|
+
'type' | 'impact' | 'relationship' | 'resolution'
|
|
18
|
+
> {
|
|
16
19
|
/**
|
|
17
20
|
* The field that did not pass validation.
|
|
18
21
|
*/
|
|
@@ -55,8 +58,8 @@ export interface DomainValidation extends Omit<DomainImpactItem, 'type' | 'impac
|
|
|
55
58
|
*
|
|
56
59
|
* @param property The property to validate
|
|
57
60
|
*/
|
|
58
|
-
export function validatePropertyName(property: DomainProperty | DomainAssociation):
|
|
59
|
-
const results:
|
|
61
|
+
export function validatePropertyName(property: DomainProperty | DomainAssociation): DomainValidationSchema[] {
|
|
62
|
+
const results: DomainValidationSchema[] = []
|
|
60
63
|
const label = property.info.getLabel()
|
|
61
64
|
const parentEntity = property.getParentInstance() as DomainEntity
|
|
62
65
|
const type = property.kind === DomainPropertyKind ? 'property' : 'association'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DomainEntityKind } from '../../models/kinds.js'
|
|
2
2
|
import type { DataDomain } from '../DataDomain.js'
|
|
3
3
|
import { DataSemantics, isPropertySemantic, SemanticType } from '../Semantics.js'
|
|
4
|
-
import type {
|
|
4
|
+
import type { DomainValidationSchema } from './rules.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* SemanticValidation is a class that performs validation on semantics in a data domain.
|
|
@@ -14,8 +14,8 @@ export class SemanticValidation {
|
|
|
14
14
|
* Performs all the semantic validation rules on the domain.
|
|
15
15
|
* @returns The list of validation messages.
|
|
16
16
|
*/
|
|
17
|
-
validate():
|
|
18
|
-
const results:
|
|
17
|
+
validate(): DomainValidationSchema[] {
|
|
18
|
+
const results: DomainValidationSchema[] = []
|
|
19
19
|
|
|
20
20
|
// Check for User entity semantic
|
|
21
21
|
const hasUserEntity = this.validateUserEntity()
|
|
@@ -40,8 +40,8 @@ export class SemanticValidation {
|
|
|
40
40
|
* Validates if there is at least one entity with the User semantic.
|
|
41
41
|
* This is a recommended semantic for authentication purposes.
|
|
42
42
|
*/
|
|
43
|
-
private validateUserEntity():
|
|
44
|
-
const results:
|
|
43
|
+
private validateUserEntity(): DomainValidationSchema[] {
|
|
44
|
+
const results: DomainValidationSchema[] = []
|
|
45
45
|
let hasUserEntity = false
|
|
46
46
|
|
|
47
47
|
for (const entity of this.domain.listEntities()) {
|
|
@@ -70,8 +70,8 @@ export class SemanticValidation {
|
|
|
70
70
|
* Validates if entities have the recommended timestamp semantics.
|
|
71
71
|
* This includes CreatedTimestamp and UpdatedTimestamp.
|
|
72
72
|
*/
|
|
73
|
-
private validateTimestampSemantics():
|
|
74
|
-
const results:
|
|
73
|
+
private validateTimestampSemantics(): DomainValidationSchema[] {
|
|
74
|
+
const results: DomainValidationSchema[] = []
|
|
75
75
|
|
|
76
76
|
for (const entity of this.domain.listEntities()) {
|
|
77
77
|
let hasCreatedTimestamp = false
|
|
@@ -118,8 +118,8 @@ export class SemanticValidation {
|
|
|
118
118
|
* Validates if entities have the recommended soft delete semantics.
|
|
119
119
|
* This includes either DeletedTimestamp or DeletedFlag.
|
|
120
120
|
*/
|
|
121
|
-
private validateSoftDeleteSemantics():
|
|
122
|
-
const results:
|
|
121
|
+
private validateSoftDeleteSemantics(): DomainValidationSchema[] {
|
|
122
|
+
const results: DomainValidationSchema[] = []
|
|
123
123
|
|
|
124
124
|
for (const entity of this.domain.listEntities()) {
|
|
125
125
|
let hasSoftDelete = false
|
|
@@ -150,8 +150,8 @@ export class SemanticValidation {
|
|
|
150
150
|
/**
|
|
151
151
|
* Validates if property semantics are applied to properties with compatible data types.
|
|
152
152
|
*/
|
|
153
|
-
private validatePropertySemanticsDataTypes():
|
|
154
|
-
const results:
|
|
153
|
+
private validatePropertySemanticsDataTypes(): DomainValidationSchema[] {
|
|
154
|
+
const results: DomainValidationSchema[] = []
|
|
155
155
|
|
|
156
156
|
for (const entity of this.domain.listEntities()) {
|
|
157
157
|
for (const property of entity.listProperties()) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { DomainValidation } from '../../../src/modeling/DomainValidation.js'
|
|
3
|
-
import { DataDomain
|
|
3
|
+
import { DataDomain } from '../../../src/index.js'
|
|
4
4
|
import { SemanticType } from '../../../src/modeling/Semantics.js'
|
|
5
5
|
|
|
6
6
|
test.group('DomainImpactAnalysis.validate()', (group) => {
|
|
@@ -14,12 +14,7 @@ test.group('DomainImpactAnalysis.validate()', (group) => {
|
|
|
14
14
|
|
|
15
15
|
test('validate() should return an empty report when the domain is empty', ({ assert }) => {
|
|
16
16
|
const report = analysis.validate()
|
|
17
|
-
assert.deepEqual(report,
|
|
18
|
-
key: '',
|
|
19
|
-
kind: DataDomainKind,
|
|
20
|
-
impact: [],
|
|
21
|
-
canProceed: true,
|
|
22
|
-
})
|
|
17
|
+
assert.deepEqual(report, [])
|
|
23
18
|
})
|
|
24
19
|
|
|
25
20
|
test('validate() should return validation errors for entities', ({ assert }) => {
|
|
@@ -27,7 +22,7 @@ test.group('DomainImpactAnalysis.validate()', (group) => {
|
|
|
27
22
|
model.addEntity({ key: 'entity', info: { name: 'Invalid-Name' } }) // Invalid name
|
|
28
23
|
|
|
29
24
|
const report = analysis.validate()
|
|
30
|
-
assert.lengthOf(report
|
|
25
|
+
assert.lengthOf(report, 8)
|
|
31
26
|
// we test specific rules in the validation tests
|
|
32
27
|
})
|
|
33
28
|
|
|
@@ -37,7 +32,7 @@ test.group('DomainImpactAnalysis.validate()', (group) => {
|
|
|
37
32
|
entity.addProperty({ key: 'invalid-property', type: 'string', info: { name: 'invalid-property' } })
|
|
38
33
|
|
|
39
34
|
const report = analysis.validate()
|
|
40
|
-
assert.lengthOf(report
|
|
35
|
+
assert.lengthOf(report, 6)
|
|
41
36
|
// we test specific rules in the validation tests
|
|
42
37
|
})
|
|
43
38
|
|
|
@@ -48,7 +43,7 @@ test.group('DomainImpactAnalysis.validate()', (group) => {
|
|
|
48
43
|
entity1.addAssociation({ targets: [{ key: entity2.key }], info: { name: 'Invalid-Name' } })
|
|
49
44
|
|
|
50
45
|
const report = analysis.validate()
|
|
51
|
-
assert.lengthOf(report
|
|
46
|
+
assert.lengthOf(report, 12)
|
|
52
47
|
// we test specific rules in the validation tests
|
|
53
48
|
})
|
|
54
49
|
|
|
@@ -60,7 +55,7 @@ test.group('DomainImpactAnalysis.validate()', (group) => {
|
|
|
60
55
|
entity1.addAssociation({ targets: [{ key: entity2.key }], info: { name: 'Invalid-Name' } })
|
|
61
56
|
|
|
62
57
|
const report = analysis.validate()
|
|
63
|
-
assert.lengthOf(report
|
|
58
|
+
assert.lengthOf(report, 16)
|
|
64
59
|
// we test specific rules in the validation tests
|
|
65
60
|
})
|
|
66
61
|
|
|
@@ -88,7 +83,6 @@ test.group('DomainImpactAnalysis.validate()', (group) => {
|
|
|
88
83
|
p5e1.addSemantic({ id: SemanticType.DeletedFlag })
|
|
89
84
|
|
|
90
85
|
const report = analysis.validate()
|
|
91
|
-
assert.lengthOf(report
|
|
92
|
-
assert.equal(report.canProceed, true)
|
|
86
|
+
assert.lengthOf(report, 0)
|
|
93
87
|
})
|
|
94
88
|
})
|
|
@@ -37,17 +37,17 @@ test.group('ExposedEntity', () => {
|
|
|
37
37
|
assert.throws(() => ex.setResourcePath('/products/notParam'))
|
|
38
38
|
}).tags(['@modeling', '@exposed-entity'])
|
|
39
39
|
|
|
40
|
-
test('setResourcePath without collection must have exactly
|
|
40
|
+
test('setResourcePath without collection must have exactly one segment', ({ assert }) => {
|
|
41
41
|
const model = new ApiModel()
|
|
42
42
|
const ex = new ExposedEntity(model, {
|
|
43
43
|
hasCollection: false,
|
|
44
|
-
resourcePath: '/profile
|
|
44
|
+
resourcePath: '/profile',
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
ex.setResourcePath('settings
|
|
48
|
-
assert.equal(ex.resourcePath, '/settings
|
|
47
|
+
ex.setResourcePath('settings')
|
|
48
|
+
assert.equal(ex.resourcePath, '/settings')
|
|
49
49
|
|
|
50
|
-
assert.throws(() => ex.setResourcePath('
|
|
50
|
+
assert.throws(() => ex.setResourcePath('settings/secret'))
|
|
51
51
|
}).tags(['@modeling', '@exposed-entity'])
|
|
52
52
|
|
|
53
53
|
test('computes absolute resource and collection paths along parent chain', ({ assert }) => {
|