@api-client/core 0.19.21 → 0.19.23
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 +37 -13
- package/build/src/modeling/ApiModel.js.map +1 -1
- package/build/src/modeling/ExposedEntity.d.ts +9 -0
- package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
- package/build/src/modeling/ExposedEntity.js +90 -14
- package/build/src/modeling/ExposedEntity.js.map +1 -1
- package/build/src/modeling/actions/Action.js +2 -2
- package/build/src/modeling/actions/Action.js.map +1 -1
- package/build/src/modeling/rules/AccessRule.d.ts +5 -1
- package/build/src/modeling/rules/AccessRule.d.ts.map +1 -1
- package/build/src/modeling/rules/AccessRule.js +4 -1
- package/build/src/modeling/rules/AccessRule.js.map +1 -1
- package/build/src/modeling/rules/AllowAuthenticated.d.ts +4 -1
- package/build/src/modeling/rules/AllowAuthenticated.d.ts.map +1 -1
- package/build/src/modeling/rules/AllowAuthenticated.js +2 -2
- package/build/src/modeling/rules/AllowAuthenticated.js.map +1 -1
- package/build/src/modeling/rules/AllowPublic.d.ts +4 -1
- package/build/src/modeling/rules/AllowPublic.d.ts.map +1 -1
- package/build/src/modeling/rules/AllowPublic.js +2 -2
- package/build/src/modeling/rules/AllowPublic.js.map +1 -1
- package/build/src/modeling/rules/MatchEmailDomain.d.ts +4 -1
- package/build/src/modeling/rules/MatchEmailDomain.d.ts.map +1 -1
- package/build/src/modeling/rules/MatchEmailDomain.js +2 -2
- package/build/src/modeling/rules/MatchEmailDomain.js.map +1 -1
- package/build/src/modeling/rules/MatchResourceOwner.d.ts +4 -1
- package/build/src/modeling/rules/MatchResourceOwner.d.ts.map +1 -1
- package/build/src/modeling/rules/MatchResourceOwner.js +2 -2
- package/build/src/modeling/rules/MatchResourceOwner.js.map +1 -1
- package/build/src/modeling/rules/MatchUserProperty.d.ts +4 -1
- package/build/src/modeling/rules/MatchUserProperty.d.ts.map +1 -1
- package/build/src/modeling/rules/MatchUserProperty.js +2 -2
- package/build/src/modeling/rules/MatchUserProperty.js.map +1 -1
- package/build/src/modeling/rules/MatchUserRole.d.ts +4 -1
- package/build/src/modeling/rules/MatchUserRole.d.ts.map +1 -1
- package/build/src/modeling/rules/MatchUserRole.js +2 -2
- package/build/src/modeling/rules/MatchUserRole.js.map +1 -1
- package/build/src/modeling/rules/index.d.ts +4 -1
- package/build/src/modeling/rules/index.d.ts.map +1 -1
- package/build/src/modeling/rules/index.js +7 -7
- package/build/src/modeling/rules/index.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/modeling/ApiModel.ts +37 -13
- package/src/modeling/ExposedEntity.ts +98 -15
- package/src/modeling/actions/Action.ts +2 -2
- package/src/modeling/rules/AccessRule.ts +8 -1
- package/src/modeling/rules/AllowAuthenticated.ts +5 -2
- package/src/modeling/rules/AllowPublic.ts +5 -2
- package/src/modeling/rules/MatchEmailDomain.ts +5 -2
- package/src/modeling/rules/MatchResourceOwner.ts +5 -2
- package/src/modeling/rules/MatchUserProperty.ts +5 -2
- package/src/modeling/rules/MatchUserRole.ts +5 -2
- package/tests/unit/modeling/actions/Action.spec.ts +13 -10
- package/tests/unit/modeling/actions/CreateAction.spec.ts +7 -6
- package/tests/unit/modeling/actions/DeleteAction.spec.ts +7 -6
- package/tests/unit/modeling/actions/ListAction.spec.ts +5 -4
- package/tests/unit/modeling/actions/ReadAction.spec.ts +9 -8
- package/tests/unit/modeling/actions/SearchAction.spec.ts +5 -4
- package/tests/unit/modeling/actions/UpdateAction.spec.ts +7 -6
- package/tests/unit/modeling/actions/helpers.ts +7 -0
- package/tests/unit/modeling/api_model.spec.ts +3 -1
- package/tests/unit/modeling/api_model_expose_entity.spec.ts +5 -17
- package/tests/unit/modeling/exposed_entity.spec.ts +150 -3
- package/tests/unit/modeling/exposed_entity_actions.spec.ts +0 -4
- package/tests/unit/modeling/rules/AccessRule.spec.ts +6 -5
- package/tests/unit/modeling/rules/AllowAuthenticated.spec.ts +4 -3
- package/tests/unit/modeling/rules/AllowPublic.spec.ts +4 -3
- package/tests/unit/modeling/rules/MatchEmailDomain.spec.ts +6 -5
- package/tests/unit/modeling/rules/MatchResourceOwner.spec.ts +7 -6
- package/tests/unit/modeling/rules/MatchUserProperty.spec.ts +6 -5
- package/tests/unit/modeling/rules/MatchUserRole.spec.ts +6 -5
- package/tests/unit/modeling/rules/restoring_rules.spec.ts +19 -21
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { DeleteAction } from '../../../../src/modeling/actions/DeleteAction.js'
|
|
3
|
+
import { mockExposedEntity } from './helpers.js'
|
|
3
4
|
|
|
4
5
|
test.group('DeleteAction', () => {
|
|
5
6
|
test('initializes with default values', ({ assert }) => {
|
|
6
|
-
const action = new DeleteAction(
|
|
7
|
+
const action = new DeleteAction(mockExposedEntity())
|
|
7
8
|
assert.equal(action.kind, 'delete')
|
|
8
9
|
assert.equal(action.strategy, 'soft')
|
|
9
10
|
assert.equal(action.retentionPeriod, 30)
|
|
@@ -11,7 +12,7 @@ test.group('DeleteAction', () => {
|
|
|
11
12
|
}).tags(['@modeling', '@action', '@delete-action'])
|
|
12
13
|
|
|
13
14
|
test('initializes with provided values', ({ assert }) => {
|
|
14
|
-
const action = new DeleteAction(
|
|
15
|
+
const action = new DeleteAction(mockExposedEntity(), {
|
|
15
16
|
strategy: 'hard',
|
|
16
17
|
retentionPeriod: 0,
|
|
17
18
|
accessRule: [{ type: 'allowPublic' }],
|
|
@@ -27,7 +28,7 @@ test.group('DeleteAction', () => {
|
|
|
27
28
|
test('constructor copies arrays (immutability)', ({ assert }) => {
|
|
28
29
|
const rules = [{ type: 'allowPublic' }]
|
|
29
30
|
|
|
30
|
-
const action = new DeleteAction(
|
|
31
|
+
const action = new DeleteAction(mockExposedEntity(), {
|
|
31
32
|
accessRule: rules,
|
|
32
33
|
})
|
|
33
34
|
|
|
@@ -40,7 +41,7 @@ test.group('DeleteAction', () => {
|
|
|
40
41
|
}).tags(['@modeling', '@action', '@delete-action', '@immutability'])
|
|
41
42
|
|
|
42
43
|
test('toJSON returns valid schema', ({ assert }) => {
|
|
43
|
-
const action = new DeleteAction(
|
|
44
|
+
const action = new DeleteAction(mockExposedEntity(), {
|
|
44
45
|
strategy: 'hard',
|
|
45
46
|
retentionPeriod: 0,
|
|
46
47
|
})
|
|
@@ -53,7 +54,7 @@ test.group('DeleteAction', () => {
|
|
|
53
54
|
}).tags(['@modeling', '@action', '@delete-action', '@serialization'])
|
|
54
55
|
|
|
55
56
|
test('notifies change when strategy changes', async ({ assert }) => {
|
|
56
|
-
const action = new DeleteAction(
|
|
57
|
+
const action = new DeleteAction(mockExposedEntity())
|
|
57
58
|
let notified = false
|
|
58
59
|
action.addEventListener('change', () => {
|
|
59
60
|
notified = true
|
|
@@ -65,7 +66,7 @@ test.group('DeleteAction', () => {
|
|
|
65
66
|
}).tags(['@modeling', '@action', '@delete-action', '@observed'])
|
|
66
67
|
|
|
67
68
|
test('notifies change when retentionPeriod changes', async ({ assert }) => {
|
|
68
|
-
const action = new DeleteAction(
|
|
69
|
+
const action = new DeleteAction(mockExposedEntity())
|
|
69
70
|
let notified = false
|
|
70
71
|
action.addEventListener('change', () => {
|
|
71
72
|
notified = true
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { ListAction } from '../../../../src/modeling/actions/ListAction.js'
|
|
3
|
+
import { mockExposedEntity } from './helpers.js'
|
|
3
4
|
|
|
4
5
|
test.group('ListAction', () => {
|
|
5
6
|
test('initializes with default values', ({ assert }) => {
|
|
6
|
-
const action = new ListAction(
|
|
7
|
+
const action = new ListAction(mockExposedEntity())
|
|
7
8
|
assert.equal(action.kind, 'list')
|
|
8
9
|
assert.isUndefined(action.cacheTtl)
|
|
9
10
|
assert.isEmpty(action.accessRule) // Inherited from Action
|
|
@@ -12,7 +13,7 @@ test.group('ListAction', () => {
|
|
|
12
13
|
test('initializes with provided values', ({ assert }) => {
|
|
13
14
|
const cacheTtl = 100
|
|
14
15
|
|
|
15
|
-
const action = new ListAction(
|
|
16
|
+
const action = new ListAction(mockExposedEntity(), {
|
|
16
17
|
cacheTtl,
|
|
17
18
|
})
|
|
18
19
|
|
|
@@ -20,7 +21,7 @@ test.group('ListAction', () => {
|
|
|
20
21
|
}).tags(['@modeling', '@action', '@list-action'])
|
|
21
22
|
|
|
22
23
|
test('toJSON returns safe copy', ({ assert }) => {
|
|
23
|
-
const action = new ListAction(
|
|
24
|
+
const action = new ListAction(mockExposedEntity(), {
|
|
24
25
|
cacheTtl: 100,
|
|
25
26
|
})
|
|
26
27
|
|
|
@@ -33,7 +34,7 @@ test.group('ListAction', () => {
|
|
|
33
34
|
}).tags(['@modeling', '@action', '@list-action', '@immutability'])
|
|
34
35
|
|
|
35
36
|
test('notifies change when cacheTtl changes', async ({ assert }) => {
|
|
36
|
-
const action = new ListAction(
|
|
37
|
+
const action = new ListAction(mockExposedEntity())
|
|
37
38
|
let notified = false
|
|
38
39
|
action.addEventListener('change', () => {
|
|
39
40
|
notified = true
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { ReadAction } from '../../../../src/modeling/actions/ReadAction.js'
|
|
3
3
|
import { AccessRule } from '../../../../src/modeling/rules/index.js'
|
|
4
|
+
import { mockExposedEntity } from './helpers.js'
|
|
4
5
|
|
|
5
6
|
test.group('ReadAction', () => {
|
|
6
7
|
test('initializes with default values', ({ assert }) => {
|
|
7
|
-
const action = new ReadAction(
|
|
8
|
+
const action = new ReadAction(mockExposedEntity())
|
|
8
9
|
assert.equal(action.kind, 'read')
|
|
9
10
|
assert.isEmpty(action.accessRule) // Inherited from Action
|
|
10
11
|
}).tags(['@modeling', '@action', '@read-action'])
|
|
11
12
|
|
|
12
13
|
test('initializes with inherited values', ({ assert }) => {
|
|
13
|
-
const action = new ReadAction(
|
|
14
|
+
const action = new ReadAction(mockExposedEntity(), {
|
|
14
15
|
accessRule: [{ type: 'allowPublic' }],
|
|
15
16
|
})
|
|
16
17
|
|
|
@@ -22,7 +23,7 @@ test.group('ReadAction', () => {
|
|
|
22
23
|
test('constructor copies arrays (immutability)', ({ assert }) => {
|
|
23
24
|
const rules = [{ type: 'allowPublic' }]
|
|
24
25
|
|
|
25
|
-
const action = new ReadAction(
|
|
26
|
+
const action = new ReadAction(mockExposedEntity(), {
|
|
26
27
|
accessRule: rules,
|
|
27
28
|
})
|
|
28
29
|
|
|
@@ -35,7 +36,7 @@ test.group('ReadAction', () => {
|
|
|
35
36
|
}).tags(['@modeling', '@action', '@read-action', '@immutability'])
|
|
36
37
|
|
|
37
38
|
test('toJSON returns valid schema', ({ assert }) => {
|
|
38
|
-
const action = new ReadAction(
|
|
39
|
+
const action = new ReadAction(mockExposedEntity(), {
|
|
39
40
|
accessRule: [{ type: 'allowPublic' }],
|
|
40
41
|
})
|
|
41
42
|
|
|
@@ -51,26 +52,26 @@ test.group('ReadAction', () => {
|
|
|
51
52
|
}).tags(['@modeling', '@action', '@read-action', '@serialization'])
|
|
52
53
|
|
|
53
54
|
test('notifies change when inherited property changes', async ({ assert }) => {
|
|
54
|
-
const action = new ReadAction(
|
|
55
|
+
const action = new ReadAction(mockExposedEntity())
|
|
55
56
|
let notified = false
|
|
56
57
|
action.addEventListener('change', () => {
|
|
57
58
|
notified = true
|
|
58
59
|
})
|
|
59
60
|
|
|
60
61
|
// Modify inherited property
|
|
61
|
-
action.accessRule = [new AccessRule({ type: 'allowPublic' })]
|
|
62
|
+
action.accessRule = [new AccessRule(mockExposedEntity(), { type: 'allowPublic' })]
|
|
62
63
|
await Promise.resolve()
|
|
63
64
|
assert.isTrue(notified)
|
|
64
65
|
}).tags(['@modeling', '@action', '@read-action', '@observed'])
|
|
65
66
|
|
|
66
67
|
test('notifies change when accessRule value change', async ({ assert }) => {
|
|
67
|
-
const action = new ReadAction(
|
|
68
|
+
const action = new ReadAction(mockExposedEntity())
|
|
68
69
|
let notified = false
|
|
69
70
|
action.addEventListener('change', () => {
|
|
70
71
|
notified = true
|
|
71
72
|
})
|
|
72
73
|
|
|
73
|
-
action.accessRule.push(new AccessRule({ type: 'allowPublic' }))
|
|
74
|
+
action.accessRule.push(new AccessRule(mockExposedEntity(), { type: 'allowPublic' }))
|
|
74
75
|
await Promise.resolve()
|
|
75
76
|
assert.isTrue(notified)
|
|
76
77
|
}).tags(['@modeling', '@action', '@read-action', '@observed'])
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { SearchAction } from '../../../../src/modeling/actions/SearchAction.js'
|
|
3
|
+
import { mockExposedEntity } from './helpers.js'
|
|
3
4
|
|
|
4
5
|
test.group('SearchAction', () => {
|
|
5
6
|
test('initializes with default values', ({ assert }) => {
|
|
6
|
-
const action = new SearchAction(
|
|
7
|
+
const action = new SearchAction(mockExposedEntity())
|
|
7
8
|
assert.equal(action.kind, 'search')
|
|
8
9
|
assert.isUndefined(action.maxAstDepth)
|
|
9
10
|
assert.isEmpty(action.accessRule)
|
|
@@ -11,7 +12,7 @@ test.group('SearchAction', () => {
|
|
|
11
12
|
|
|
12
13
|
test('initializes with provided values', ({ assert }) => {
|
|
13
14
|
const maxAstDepth = 10
|
|
14
|
-
const action = new SearchAction(
|
|
15
|
+
const action = new SearchAction(mockExposedEntity(), {
|
|
15
16
|
maxAstDepth,
|
|
16
17
|
})
|
|
17
18
|
|
|
@@ -20,7 +21,7 @@ test.group('SearchAction', () => {
|
|
|
20
21
|
}).tags(['@modeling', '@action', '@search-action'])
|
|
21
22
|
|
|
22
23
|
test('toJSON returns valid schema', ({ assert }) => {
|
|
23
|
-
const action = new SearchAction(
|
|
24
|
+
const action = new SearchAction(mockExposedEntity(), {
|
|
24
25
|
maxAstDepth: 10,
|
|
25
26
|
})
|
|
26
27
|
|
|
@@ -34,7 +35,7 @@ test.group('SearchAction', () => {
|
|
|
34
35
|
}).tags(['@modeling', '@action', '@search-action', '@serialization', '@immutability'])
|
|
35
36
|
|
|
36
37
|
test('notifies change when maxAstDepth changes', async ({ assert }) => {
|
|
37
|
-
const action = new SearchAction(
|
|
38
|
+
const action = new SearchAction(mockExposedEntity())
|
|
38
39
|
let notified = false
|
|
39
40
|
action.addEventListener('change', () => {
|
|
40
41
|
notified = true
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { UpdateAction } from '../../../../src/modeling/actions/UpdateAction.js'
|
|
3
|
+
import { mockExposedEntity } from './helpers.js'
|
|
3
4
|
|
|
4
5
|
test.group('UpdateAction', () => {
|
|
5
6
|
test('initializes with default values', ({ assert }) => {
|
|
6
|
-
const action = new UpdateAction(
|
|
7
|
+
const action = new UpdateAction(mockExposedEntity())
|
|
7
8
|
assert.equal(action.kind, 'update')
|
|
8
9
|
assert.deepEqual(action.allowedMethods, ['PATCH'])
|
|
9
10
|
assert.isEmpty(action.accessRule)
|
|
@@ -11,7 +12,7 @@ test.group('UpdateAction', () => {
|
|
|
11
12
|
|
|
12
13
|
test('initializes with provided values', ({ assert }) => {
|
|
13
14
|
const methods: ('PUT' | 'PATCH')[] = ['PUT', 'PATCH']
|
|
14
|
-
const action = new UpdateAction(
|
|
15
|
+
const action = new UpdateAction(mockExposedEntity(), {
|
|
15
16
|
allowedMethods: methods,
|
|
16
17
|
})
|
|
17
18
|
|
|
@@ -22,7 +23,7 @@ test.group('UpdateAction', () => {
|
|
|
22
23
|
test('constructor copies arrays (immutability)', ({ assert }) => {
|
|
23
24
|
const methods: ('PUT' | 'PATCH')[] = ['PUT']
|
|
24
25
|
|
|
25
|
-
const action = new UpdateAction(
|
|
26
|
+
const action = new UpdateAction(mockExposedEntity(), {
|
|
26
27
|
allowedMethods: methods,
|
|
27
28
|
})
|
|
28
29
|
|
|
@@ -33,7 +34,7 @@ test.group('UpdateAction', () => {
|
|
|
33
34
|
}).tags(['@modeling', '@action', '@update-action', '@immutability'])
|
|
34
35
|
|
|
35
36
|
test('toJSON returns valid schema', ({ assert }) => {
|
|
36
|
-
const action = new UpdateAction(
|
|
37
|
+
const action = new UpdateAction(mockExposedEntity(), {
|
|
37
38
|
allowedMethods: ['PUT'],
|
|
38
39
|
})
|
|
39
40
|
|
|
@@ -48,7 +49,7 @@ test.group('UpdateAction', () => {
|
|
|
48
49
|
}).tags(['@modeling', '@action', '@update-action', '@serialization', '@immutability'])
|
|
49
50
|
|
|
50
51
|
test('notifies change when allowedMethods changes', async ({ assert }) => {
|
|
51
|
-
const action = new UpdateAction(
|
|
52
|
+
const action = new UpdateAction(mockExposedEntity())
|
|
52
53
|
let notified = false
|
|
53
54
|
action.addEventListener('change', () => {
|
|
54
55
|
notified = true
|
|
@@ -60,7 +61,7 @@ test.group('UpdateAction', () => {
|
|
|
60
61
|
}).tags(['@modeling', '@action', '@update-action', '@observed'])
|
|
61
62
|
|
|
62
63
|
test('notifies change when allowedMethods value change', async ({ assert }) => {
|
|
63
|
-
const action = new UpdateAction(
|
|
64
|
+
const action = new UpdateAction(mockExposedEntity())
|
|
64
65
|
let notified = false
|
|
65
66
|
action.addEventListener('change', () => {
|
|
66
67
|
notified = true
|
|
@@ -152,7 +152,7 @@ test.group('ApiModel.constructor()', () => {
|
|
|
152
152
|
assert.deepEqual(model.authentication, { strategy: 'UsernamePassword' })
|
|
153
153
|
assert.deepEqual(model.authorization, { strategy: 'RBAC', roleKey: 'role' })
|
|
154
154
|
assert.deepEqual(model.session, { secret: 'secret', properties: ['email'] })
|
|
155
|
-
assert.deepEqual(model.accessRule, [new AllowPublicAccessRule()])
|
|
155
|
+
assert.deepEqual(model.accessRule, [new AllowPublicAccessRule(model)])
|
|
156
156
|
assert.deepEqual(model.rateLimiting, new RateLimitingConfiguration())
|
|
157
157
|
assert.equal(model.termsOfService, 'https://example.com/terms')
|
|
158
158
|
assert.deepEqual(model.contact, { name: 'John Doe', email: 'john.doe@example.com' })
|
|
@@ -223,6 +223,8 @@ test.group('ApiModel.constructor()', () => {
|
|
|
223
223
|
})
|
|
224
224
|
|
|
225
225
|
Array.from(model.exposes.values())[0]!.actions[0]!.kind = 'write'
|
|
226
|
+
// there are two microtasks on the notification path.
|
|
227
|
+
await Promise.resolve()
|
|
226
228
|
await Promise.resolve()
|
|
227
229
|
assert.isTrue(notified)
|
|
228
230
|
}).tags(['@modeling', '@api', '@observed'])
|
|
@@ -35,20 +35,6 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
35
35
|
assert.deepEqual(exposedEntity.actions, [])
|
|
36
36
|
}).tags(['@modeling', '@api'])
|
|
37
37
|
|
|
38
|
-
test('returns an existing entity if already exposed', ({ assert }) => {
|
|
39
|
-
const domain = new DataDomain()
|
|
40
|
-
domain.info.version = '1.0.0'
|
|
41
|
-
const dm = domain.addModel()
|
|
42
|
-
const e1 = dm.addEntity()
|
|
43
|
-
const model = new ApiModel()
|
|
44
|
-
model.attachDataDomain(domain)
|
|
45
|
-
const initialExposedEntity = model.exposeEntity({ key: e1.key })
|
|
46
|
-
const retrievedExposedEntity = model.exposeEntity({ key: e1.key })
|
|
47
|
-
|
|
48
|
-
assert.deepEqual(retrievedExposedEntity.toJSON(), initialExposedEntity.toJSON())
|
|
49
|
-
assert.equal(model.exposes.size, 1)
|
|
50
|
-
}).tags(['@modeling', '@api'])
|
|
51
|
-
|
|
52
38
|
test('notifies change when a new entity is exposed', async ({ assert }) => {
|
|
53
39
|
const domain = new DataDomain()
|
|
54
40
|
domain.info.version = '1.0.0'
|
|
@@ -65,7 +51,7 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
65
51
|
assert.isTrue(notified)
|
|
66
52
|
}).tags(['@modeling', '@api'])
|
|
67
53
|
|
|
68
|
-
test('
|
|
54
|
+
test('throws if entity already exposed', async ({ assert }) => {
|
|
69
55
|
const domain = new DataDomain()
|
|
70
56
|
domain.info.version = '1.0.0'
|
|
71
57
|
const dm = domain.addModel()
|
|
@@ -78,9 +64,11 @@ test.group('ApiModel.exposeEntity()', () => {
|
|
|
78
64
|
model.addEventListener('change', () => {
|
|
79
65
|
notified = true
|
|
80
66
|
})
|
|
81
|
-
|
|
67
|
+
await assert.rejects(async () => {
|
|
68
|
+
model.exposeEntity({ key: e1.key }) // Second exposure
|
|
69
|
+
}, `Entity ${e1.key} is already exposed.`)
|
|
82
70
|
await Promise.resolve() // Allow microtask to run
|
|
83
|
-
assert.isFalse(notified)
|
|
71
|
+
assert.isFalse(notified, 'should not notify change if entity already exposed')
|
|
84
72
|
}).tags(['@modeling', '@api'])
|
|
85
73
|
|
|
86
74
|
test('exposes nested entities through associations', ({ assert }) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
|
-
import { ApiModel, ExposedEntity, type ExposedEntitySchema } from '../../../src/index.js'
|
|
2
|
+
import { ApiModel, DataDomain, ExposedEntity, type ExposedEntitySchema } from '../../../src/index.js'
|
|
3
3
|
import { AccessRule, RateLimitingConfiguration } from '../../../src/modeling/index.js'
|
|
4
4
|
import { ExposedEntityKind } from '../../../src/models/kinds.js'
|
|
5
5
|
|
|
@@ -127,7 +127,9 @@ test.group('ExposedEntity', () => {
|
|
|
127
127
|
|
|
128
128
|
const ex = Array.from(model.exposes.values())[0]
|
|
129
129
|
ex.setCollectionPath('items')
|
|
130
|
-
|
|
130
|
+
// there are two microtasks on the notification path.
|
|
131
|
+
await Promise.resolve()
|
|
132
|
+
await Promise.resolve()
|
|
131
133
|
assert.isAtLeast(notified, 1)
|
|
132
134
|
}).tags(['@modeling', '@exposed-entity', '@observed'])
|
|
133
135
|
|
|
@@ -154,6 +156,8 @@ test.group('ExposedEntity', () => {
|
|
|
154
156
|
|
|
155
157
|
const ex = Array.from(model.exposes.values())[0]
|
|
156
158
|
ex.setResourcePath('/products/{productId}')
|
|
159
|
+
// there are two microtasks on the notification path.
|
|
160
|
+
await Promise.resolve()
|
|
157
161
|
await Promise.resolve()
|
|
158
162
|
assert.isAtLeast(notified, 1)
|
|
159
163
|
}).tags(['@modeling', '@exposed-entity', '@observed'])
|
|
@@ -304,7 +308,7 @@ test.group('ExposedEntity', () => {
|
|
|
304
308
|
|
|
305
309
|
test('getAllRules() aggregates rules from entity, parent, and API', ({ assert }) => {
|
|
306
310
|
const model = new ApiModel()
|
|
307
|
-
model.accessRule = [new AccessRule({ type: 'allowPublic' })]
|
|
311
|
+
model.accessRule = [new AccessRule(model, { type: 'allowPublic' })]
|
|
308
312
|
|
|
309
313
|
const rootEx = new ExposedEntity(model, {
|
|
310
314
|
key: 'root',
|
|
@@ -417,3 +421,146 @@ test.group('ExposedEntity', () => {
|
|
|
417
421
|
assert.equal(childRules[3].rate, 20)
|
|
418
422
|
}).tags(['@modeling', '@exposed-entity', '@rate-limiting'])
|
|
419
423
|
})
|
|
424
|
+
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// Helper: build a DataDomain + ApiModel with a single entity already attached.
|
|
427
|
+
// Returns the domain, api model, and the entity key so tests can add properties
|
|
428
|
+
// and then call createPaginationContract() without repeating boilerplate.
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
function makeFixture() {
|
|
431
|
+
const domain = new DataDomain()
|
|
432
|
+
domain.info.version = '1.0.0'
|
|
433
|
+
const dm = domain.addModel()
|
|
434
|
+
const entity = domain.addEntity(dm.key)
|
|
435
|
+
const model = new ApiModel()
|
|
436
|
+
model.attachDataDomain(domain)
|
|
437
|
+
return { domain, model, entity }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
test.group('ExposedEntity.createPaginationContract()', () => {
|
|
441
|
+
test('throws when no domain is attached to the API model', ({ assert }) => {
|
|
442
|
+
const model = new ApiModel() // no domain attached
|
|
443
|
+
const ex = new ExposedEntity(model, { entity: { key: 'some-entity' } })
|
|
444
|
+
|
|
445
|
+
assert.throws(() => ex.createPaginationContract(), 'Entity "some-entity" not found')
|
|
446
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
447
|
+
|
|
448
|
+
test('throws when the entity key does not exist in the domain', ({ assert }) => {
|
|
449
|
+
const { model } = makeFixture()
|
|
450
|
+
const ex = new ExposedEntity(model, { entity: { key: 'non-existent' } })
|
|
451
|
+
|
|
452
|
+
assert.throws(() => ex.createPaginationContract(), 'Entity "non-existent" not found')
|
|
453
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
454
|
+
|
|
455
|
+
test('creates a fresh paginationContract when one is not yet set', ({ assert }) => {
|
|
456
|
+
const { model, entity } = makeFixture()
|
|
457
|
+
const ex = new ExposedEntity(model, { entity: { key: entity.key } })
|
|
458
|
+
|
|
459
|
+
assert.isUndefined(ex.paginationContract)
|
|
460
|
+
ex.createPaginationContract()
|
|
461
|
+
|
|
462
|
+
assert.isDefined(ex.paginationContract)
|
|
463
|
+
assert.deepEqual(ex.paginationContract!.filterableFields, [])
|
|
464
|
+
assert.deepEqual(ex.paginationContract!.sortableFields, [])
|
|
465
|
+
assert.deepEqual(ex.paginationContract!.searchableFields, [])
|
|
466
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
467
|
+
|
|
468
|
+
test('indexed props are added to filterableFields and sortableFields', ({ assert }) => {
|
|
469
|
+
const { domain, model, entity } = makeFixture()
|
|
470
|
+
domain.addProperty(entity.key, { key: 'status', index: true })
|
|
471
|
+
domain.addProperty(entity.key, { key: 'createdAt', index: true })
|
|
472
|
+
|
|
473
|
+
const ex = new ExposedEntity(model, { entity: { key: entity.key } })
|
|
474
|
+
ex.createPaginationContract()
|
|
475
|
+
|
|
476
|
+
assert.deepEqual(ex.paginationContract!.filterableFields, ['status', 'createdAt'])
|
|
477
|
+
assert.deepEqual(ex.paginationContract!.sortableFields, ['status', 'createdAt'])
|
|
478
|
+
assert.deepEqual(ex.paginationContract!.searchableFields, [])
|
|
479
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
480
|
+
|
|
481
|
+
test('search props are added to searchableFields only', ({ assert }) => {
|
|
482
|
+
const { domain, model, entity } = makeFixture()
|
|
483
|
+
domain.addProperty(entity.key, { key: 'name', search: true })
|
|
484
|
+
domain.addProperty(entity.key, { key: 'description', search: true })
|
|
485
|
+
|
|
486
|
+
const ex = new ExposedEntity(model, { entity: { key: entity.key } })
|
|
487
|
+
ex.createPaginationContract()
|
|
488
|
+
|
|
489
|
+
assert.deepEqual(ex.paginationContract!.searchableFields, ['name', 'description'])
|
|
490
|
+
assert.deepEqual(ex.paginationContract!.filterableFields, [])
|
|
491
|
+
assert.deepEqual(ex.paginationContract!.sortableFields, [])
|
|
492
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
493
|
+
|
|
494
|
+
test('a prop with both index and search appears in all three lists', ({ assert }) => {
|
|
495
|
+
const { domain, model, entity } = makeFixture()
|
|
496
|
+
domain.addProperty(entity.key, { key: 'email', index: true, search: true })
|
|
497
|
+
|
|
498
|
+
const ex = new ExposedEntity(model, { entity: { key: entity.key } })
|
|
499
|
+
ex.createPaginationContract()
|
|
500
|
+
|
|
501
|
+
assert.include(ex.paginationContract!.filterableFields, 'email')
|
|
502
|
+
assert.include(ex.paginationContract!.sortableFields, 'email')
|
|
503
|
+
assert.include(ex.paginationContract!.searchableFields, 'email')
|
|
504
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
505
|
+
|
|
506
|
+
test('props without index or search are not added to any list', ({ assert }) => {
|
|
507
|
+
const { domain, model, entity } = makeFixture()
|
|
508
|
+
domain.addProperty(entity.key, { key: 'bio' }) // no index, no search
|
|
509
|
+
|
|
510
|
+
const ex = new ExposedEntity(model, { entity: { key: entity.key } })
|
|
511
|
+
ex.createPaginationContract()
|
|
512
|
+
|
|
513
|
+
assert.deepEqual(ex.paginationContract!.filterableFields, [])
|
|
514
|
+
assert.deepEqual(ex.paginationContract!.sortableFields, [])
|
|
515
|
+
assert.deepEqual(ex.paginationContract!.searchableFields, [])
|
|
516
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
517
|
+
|
|
518
|
+
test('resets existing contract arrays before repopulating (destructive)', ({ assert }) => {
|
|
519
|
+
const { domain, model, entity } = makeFixture()
|
|
520
|
+
domain.addProperty(entity.key, { key: 'title', index: true, search: true })
|
|
521
|
+
|
|
522
|
+
const ex = new ExposedEntity(model, {
|
|
523
|
+
entity: { key: entity.key },
|
|
524
|
+
paginationContract: {
|
|
525
|
+
filterableFields: ['stale-filter'],
|
|
526
|
+
sortableFields: ['stale-sort'],
|
|
527
|
+
searchableFields: ['stale-search'],
|
|
528
|
+
},
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
// Sanity check: pre-existing stale values are present
|
|
532
|
+
assert.include(ex.paginationContract!.filterableFields, 'stale-filter')
|
|
533
|
+
|
|
534
|
+
ex.createPaginationContract()
|
|
535
|
+
|
|
536
|
+
// Stale values are gone; only the freshly scanned fields remain
|
|
537
|
+
assert.deepEqual(ex.paginationContract!.filterableFields, ['title'])
|
|
538
|
+
assert.deepEqual(ex.paginationContract!.sortableFields, ['title'])
|
|
539
|
+
assert.deepEqual(ex.paginationContract!.searchableFields, ['title'])
|
|
540
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
541
|
+
|
|
542
|
+
test('entity with no properties produces empty contract lists', ({ assert }) => {
|
|
543
|
+
const { model, entity } = makeFixture() // entity has no props
|
|
544
|
+
|
|
545
|
+
const ex = new ExposedEntity(model, { entity: { key: entity.key } })
|
|
546
|
+
ex.createPaginationContract()
|
|
547
|
+
|
|
548
|
+
assert.deepEqual(ex.paginationContract!.filterableFields, [])
|
|
549
|
+
assert.deepEqual(ex.paginationContract!.sortableFields, [])
|
|
550
|
+
assert.deepEqual(ex.paginationContract!.searchableFields, [])
|
|
551
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
552
|
+
|
|
553
|
+
test('mixed props: only the flagged ones appear in the relevant lists', ({ assert }) => {
|
|
554
|
+
const { domain, model, entity } = makeFixture()
|
|
555
|
+
domain.addProperty(entity.key, { key: 'id', index: true }) // filterable + sortable
|
|
556
|
+
domain.addProperty(entity.key, { key: 'body', search: true }) // searchable
|
|
557
|
+
domain.addProperty(entity.key, { key: 'metadata' }) // neither
|
|
558
|
+
|
|
559
|
+
const ex = new ExposedEntity(model, { entity: { key: entity.key } })
|
|
560
|
+
ex.createPaginationContract()
|
|
561
|
+
|
|
562
|
+
assert.deepEqual(ex.paginationContract!.filterableFields, ['id'])
|
|
563
|
+
assert.deepEqual(ex.paginationContract!.sortableFields, ['id'])
|
|
564
|
+
assert.deepEqual(ex.paginationContract!.searchableFields, ['body'])
|
|
565
|
+
}).tags(['@modeling', '@exposed-entity', '@pagination'])
|
|
566
|
+
})
|
|
@@ -33,9 +33,6 @@ test.group('ExposedEntity::actions', (group) => {
|
|
|
33
33
|
test('restores a list acton', ({ assert }) => {
|
|
34
34
|
const action: ListActionSchema = {
|
|
35
35
|
kind: 'list',
|
|
36
|
-
pagination: { kind: '' },
|
|
37
|
-
sortableFields: [],
|
|
38
|
-
filterableFields: [],
|
|
39
36
|
}
|
|
40
37
|
const model = new ApiModel(
|
|
41
38
|
{
|
|
@@ -177,7 +174,6 @@ test.group('ExposedEntity::actions', (group) => {
|
|
|
177
174
|
test('restores a search acton', ({ assert }) => {
|
|
178
175
|
const action: SearchActionSchema = {
|
|
179
176
|
kind: 'search',
|
|
180
|
-
fields: [],
|
|
181
177
|
}
|
|
182
178
|
const model = new ApiModel(
|
|
183
179
|
{
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { AccessRule, type AccessRuleSchema } from '../../../../src/modeling/index.js'
|
|
3
|
+
import { mockExposedEntity } from '../actions/helpers.js'
|
|
3
4
|
|
|
4
5
|
test.group('AccessRule', () => {
|
|
5
6
|
test('initializes with default values', ({ assert }) => {
|
|
6
|
-
const rule = new AccessRule()
|
|
7
|
+
const rule = new AccessRule(mockExposedEntity())
|
|
7
8
|
assert.equal(rule.type, '')
|
|
8
9
|
}).tags(['@modeling', '@access-rule'])
|
|
9
10
|
|
|
10
11
|
test('initializes with provided values', ({ assert }) => {
|
|
11
12
|
const schema: AccessRuleSchema = { type: 'public' }
|
|
12
|
-
const rule = new AccessRule(schema)
|
|
13
|
+
const rule = new AccessRule(mockExposedEntity(), schema)
|
|
13
14
|
assert.equal(rule.type, 'public')
|
|
14
15
|
}).tags(['@modeling', '@access-rule'])
|
|
15
16
|
|
|
16
17
|
test('serializes to JSON', ({ assert }) => {
|
|
17
|
-
const rule = new AccessRule({ type: 'authenticated' })
|
|
18
|
+
const rule = new AccessRule(mockExposedEntity(), { type: 'authenticated' })
|
|
18
19
|
const json = rule.toJSON()
|
|
19
20
|
assert.deepEqual(json, { type: 'authenticated' })
|
|
20
21
|
}).tags(['@modeling', '@access-rule'])
|
|
21
22
|
|
|
22
23
|
test('notifies change', async ({ assert }) => {
|
|
23
|
-
const rule = new AccessRule({ type: 'public' })
|
|
24
|
+
const rule = new AccessRule(mockExposedEntity(), { type: 'public' })
|
|
24
25
|
let notified = false
|
|
25
26
|
rule.addEventListener('change', () => {
|
|
26
27
|
notified = true
|
|
@@ -31,7 +32,7 @@ test.group('AccessRule', () => {
|
|
|
31
32
|
}).tags(['@modeling', '@access-rule'])
|
|
32
33
|
|
|
33
34
|
test('toJSON returns safe copy (immutability)', ({ assert }) => {
|
|
34
|
-
const rule = new AccessRule({ type: 'public' })
|
|
35
|
+
const rule = new AccessRule(mockExposedEntity(), { type: 'public' })
|
|
35
36
|
const json = rule.toJSON()
|
|
36
37
|
|
|
37
38
|
// Modify JSON (simulate runtime mutation)
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { AllowAuthenticatedAccessRule } from '../../../../src/modeling/rules/AllowAuthenticated.js'
|
|
3
|
+
import { mockExposedEntity } from '../actions/helpers.js'
|
|
3
4
|
|
|
4
5
|
test.group('AllowAuthenticatedAccessRule', () => {
|
|
5
6
|
test('initializes with correct type', ({ assert }) => {
|
|
6
|
-
const rule = new AllowAuthenticatedAccessRule()
|
|
7
|
+
const rule = new AllowAuthenticatedAccessRule(mockExposedEntity())
|
|
7
8
|
assert.equal(rule.type, 'allowAuthenticated')
|
|
8
9
|
}).tags(['@modeling', '@rule', '@allow-authenticated'])
|
|
9
10
|
|
|
10
11
|
test('toJSON returns valid schema', ({ assert }) => {
|
|
11
|
-
const rule = new AllowAuthenticatedAccessRule()
|
|
12
|
+
const rule = new AllowAuthenticatedAccessRule(mockExposedEntity())
|
|
12
13
|
const json = rule.toJSON()
|
|
13
14
|
|
|
14
15
|
assert.equal(json.type, 'allowAuthenticated')
|
|
15
16
|
}).tags(['@modeling', '@rule', '@allow-authenticated', '@serialization'])
|
|
16
17
|
|
|
17
18
|
test('notifies change', async ({ assert }) => {
|
|
18
|
-
const rule = new AllowAuthenticatedAccessRule()
|
|
19
|
+
const rule = new AllowAuthenticatedAccessRule(mockExposedEntity())
|
|
19
20
|
let notified = false
|
|
20
21
|
rule.addEventListener('change', () => {
|
|
21
22
|
notified = true
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { test } from '@japa/runner'
|
|
2
2
|
import { AllowPublicAccessRule } from '../../../../src/modeling/rules/AllowPublic.js'
|
|
3
|
+
import { mockExposedEntity } from '../actions/helpers.js'
|
|
3
4
|
|
|
4
5
|
test.group('AllowPublicAccessRule', () => {
|
|
5
6
|
test('initializes with correct type', ({ assert }) => {
|
|
6
|
-
const rule = new AllowPublicAccessRule()
|
|
7
|
+
const rule = new AllowPublicAccessRule(mockExposedEntity())
|
|
7
8
|
assert.equal(rule.type, 'allowPublic')
|
|
8
9
|
}).tags(['@modeling', '@rule', '@allow-public'])
|
|
9
10
|
|
|
10
11
|
test('toJSON returns valid schema', ({ assert }) => {
|
|
11
|
-
const rule = new AllowPublicAccessRule()
|
|
12
|
+
const rule = new AllowPublicAccessRule(mockExposedEntity())
|
|
12
13
|
const json = rule.toJSON()
|
|
13
14
|
|
|
14
15
|
assert.equal(json.type, 'allowPublic')
|
|
15
16
|
}).tags(['@modeling', '@rule', '@allow-public', '@serialization'])
|
|
16
17
|
|
|
17
18
|
test('notifies change', async ({ assert }) => {
|
|
18
|
-
const rule = new AllowPublicAccessRule()
|
|
19
|
+
const rule = new AllowPublicAccessRule(mockExposedEntity())
|
|
19
20
|
let notified = false
|
|
20
21
|
rule.addEventListener('change', () => {
|
|
21
22
|
notified = true
|