@api-client/core 0.19.12 → 0.19.14
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/ExposedEntity.js +4 -4
- package/build/src/modeling/ExposedEntity.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 +5 -4
- package/build/src/modeling/validation/api_model_rules.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/modeling/ExposedEntity.ts +4 -4
- package/src/modeling/validation/api_model_rules.ts +5 -4
- 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 +13 -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.19.
|
|
4
|
+
"version": "0.19.14",
|
|
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",
|
|
@@ -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
|
|
|
@@ -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
|
})
|
|
@@ -600,7 +600,8 @@ export function validateExposedEntity(entity: ExposedEntity, apiModel: ApiModel)
|
|
|
600
600
|
message: `The ${action.kind} action has no security rules attached, making it entirely inaccessible or open.`,
|
|
601
601
|
suggestion: 'Allow specific user roles to access this operation.',
|
|
602
602
|
severity: 'error',
|
|
603
|
-
|
|
603
|
+
// using action.kind as the key context equivalent
|
|
604
|
+
context: { ...context, kind: 'Action', key: action.kind, parentExposedEntityKey: entity.key },
|
|
604
605
|
})
|
|
605
606
|
}
|
|
606
607
|
}
|
|
@@ -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 }) => {
|
|
@@ -43,12 +43,12 @@ test.group('ExposedEntity Path Setter Validation', () => {
|
|
|
43
43
|
|
|
44
44
|
// Set exp1 resource path to something specific
|
|
45
45
|
exp1.hasCollection = false
|
|
46
|
-
exp1.setResourcePath('/shared
|
|
46
|
+
exp1.setResourcePath('/shared')
|
|
47
47
|
|
|
48
48
|
// Try to set exp2 resource path to same
|
|
49
49
|
assert.throws(
|
|
50
|
-
() => exp2.setResourcePath('/shared
|
|
51
|
-
'Resource path "/shared
|
|
50
|
+
() => exp2.setResourcePath('/shared'),
|
|
51
|
+
'Resource path "/shared" is already in use by another root entity.'
|
|
52
52
|
)
|
|
53
53
|
})
|
|
54
54
|
|
|
@@ -67,8 +67,8 @@ test.group('ExposedEntity Path Setter Validation', () => {
|
|
|
67
67
|
assert.equal(exp1.collectionPath, '/new-path')
|
|
68
68
|
|
|
69
69
|
exp1.hasCollection = false // allow arbitrary resource path
|
|
70
|
-
exp1.setResourcePath('/custom
|
|
71
|
-
assert.equal(exp1.resourcePath, '/custom
|
|
70
|
+
exp1.setResourcePath('/custom')
|
|
71
|
+
assert.equal(exp1.resourcePath, '/custom')
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
test('does not validate collision for non-root entities', ({ assert }) => {
|
|
@@ -235,6 +235,19 @@ test.group('ApiModel Validation', () => {
|
|
|
235
235
|
assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_INVALID_RESOURCE_PATH_FORMAT'))
|
|
236
236
|
})
|
|
237
237
|
|
|
238
|
+
test('validateExposedEntity - path integrity (no collection)', ({ assert }) => {
|
|
239
|
+
const domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
240
|
+
const model = new ApiModel({}, domain)
|
|
241
|
+
const exposure = new ExposedEntity(model, {
|
|
242
|
+
entity: { key: 'fake' },
|
|
243
|
+
hasCollection: false,
|
|
244
|
+
})
|
|
245
|
+
exposure.resourcePath = '/invalid/two_segments'
|
|
246
|
+
exposure.actions = [new ReadAction(exposure)]
|
|
247
|
+
const issues = validateExposedEntity(exposure, model)
|
|
248
|
+
assert.isTrue(issues.some((i) => i.code === 'EXPOSURE_INVALID_RESOURCE_PATH_FORMAT'))
|
|
249
|
+
})
|
|
250
|
+
|
|
238
251
|
test('validateAction - List needs pagination', ({ assert }) => {
|
|
239
252
|
const model = new ApiModel()
|
|
240
253
|
const exposure = new ExposedEntity(model, { entity: { key: '1' } })
|