@api-client/core 0.19.24 → 0.19.25
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/index.d.ts +3 -1
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +3 -1
- package/build/src/index.js.map +1 -1
- package/build/src/mocking/ModelingMock.d.ts +2 -0
- package/build/src/mocking/ModelingMock.d.ts.map +1 -1
- package/build/src/mocking/ModelingMock.js +2 -0
- package/build/src/mocking/ModelingMock.js.map +1 -1
- package/build/src/mocking/lib/Deployment.d.ts +16 -0
- package/build/src/mocking/lib/Deployment.d.ts.map +1 -0
- package/build/src/mocking/lib/Deployment.js +76 -0
- package/build/src/mocking/lib/Deployment.js.map +1 -0
- package/build/src/modeling/Bindings.d.ts +4 -0
- package/build/src/modeling/Bindings.d.ts.map +1 -1
- package/build/src/modeling/Bindings.js.map +1 -1
- package/build/src/modeling/DataFormat.d.ts +1 -1
- package/build/src/modeling/DataFormat.d.ts.map +1 -1
- package/build/src/modeling/DataFormat.js +2 -0
- package/build/src/modeling/DataFormat.js.map +1 -1
- package/build/src/modeling/DomainAssociation.d.ts +7 -0
- package/build/src/modeling/DomainAssociation.d.ts.map +1 -1
- package/build/src/modeling/DomainAssociation.js +10 -0
- package/build/src/modeling/DomainAssociation.js.map +1 -1
- package/build/src/modeling/DomainEntity.d.ts +9 -1
- package/build/src/modeling/DomainEntity.d.ts.map +1 -1
- package/build/src/modeling/DomainEntity.js +26 -1
- package/build/src/modeling/DomainEntity.js.map +1 -1
- package/build/src/modeling/ExposedEntity.d.ts +12 -1
- package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
- package/build/src/modeling/ExposedEntity.js +24 -1
- package/build/src/modeling/ExposedEntity.js.map +1 -1
- package/build/src/modeling/RuntimeApiModel.d.ts +52 -0
- package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -0
- package/build/src/modeling/RuntimeApiModel.js +85 -0
- package/build/src/modeling/RuntimeApiModel.js.map +1 -0
- package/build/src/modeling/actions/index.d.ts +10 -0
- package/build/src/modeling/actions/index.d.ts.map +1 -1
- package/build/src/modeling/actions/index.js +30 -0
- package/build/src/modeling/actions/index.js.map +1 -1
- package/build/src/modeling/index.d.ts.map +1 -1
- package/build/src/modeling/index.js +1 -0
- package/build/src/modeling/index.js.map +1 -1
- package/build/src/modeling/types.d.ts +25 -1
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/src/models/kinds.d.ts +1 -0
- package/build/src/models/kinds.d.ts.map +1 -1
- package/build/src/models/kinds.js +1 -0
- package/build/src/models/kinds.js.map +1 -1
- package/build/src/models/store/Deployment.d.ts +53 -16
- package/build/src/models/store/Deployment.d.ts.map +1 -1
- package/build/src/models/store/Deployment.js +92 -34
- package/build/src/models/store/Deployment.js.map +1 -1
- package/build/src/sdk/DataCatalogSdk.d.ts.map +1 -1
- package/build/src/sdk/DataCatalogSdk.js +22 -179
- package/build/src/sdk/DataCatalogSdk.js.map +1 -1
- package/build/src/sdk/DeploymentsSdk.d.ts +48 -0
- package/build/src/sdk/DeploymentsSdk.d.ts.map +1 -0
- package/build/src/sdk/DeploymentsSdk.js +94 -0
- package/build/src/sdk/DeploymentsSdk.js.map +1 -0
- package/build/src/sdk/RouteBuilder.d.ts +2 -0
- package/build/src/sdk/RouteBuilder.d.ts.map +1 -1
- package/build/src/sdk/RouteBuilder.js +6 -0
- package/build/src/sdk/RouteBuilder.js.map +1 -1
- package/build/src/sdk/Sdk.d.ts +2 -0
- package/build/src/sdk/Sdk.d.ts.map +1 -1
- package/build/src/sdk/Sdk.js +2 -0
- package/build/src/sdk/Sdk.js.map +1 -1
- package/build/src/sdk/SdkBase.d.ts +19 -1
- package/build/src/sdk/SdkBase.d.ts.map +1 -1
- package/build/src/sdk/SdkBase.js +31 -1
- package/build/src/sdk/SdkBase.js.map +1 -1
- package/build/src/sdk/SdkMock.d.ts +9 -0
- package/build/src/sdk/SdkMock.d.ts.map +1 -1
- package/build/src/sdk/SdkMock.js +73 -0
- package/build/src/sdk/SdkMock.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/matchit.d.ts +19 -0
- package/src/mocking/ModelingMock.ts +2 -0
- package/src/mocking/lib/Deployment.ts +88 -0
- package/src/modeling/Bindings.ts +4 -0
- package/src/modeling/DataFormat.ts +4 -0
- package/src/modeling/DomainAssociation.ts +11 -0
- package/src/modeling/DomainEntity.ts +30 -1
- package/src/modeling/ExposedEntity.ts +26 -1
- package/src/modeling/RuntimeApiModel.ts +137 -0
- package/src/modeling/types.ts +26 -1
- package/src/models/kinds.ts +1 -0
- package/src/models/store/Deployment.ts +122 -45
- package/src/sdk/DataCatalogSdk.ts +22 -176
- package/src/sdk/DeploymentsSdk.ts +123 -0
- package/src/sdk/RouteBuilder.ts +8 -0
- package/src/sdk/Sdk.ts +3 -0
- package/src/sdk/SdkBase.ts +35 -3
- package/src/sdk/SdkMock.ts +103 -0
- package/tests/unit/modeling/RuntimeApiModel.spec.ts +122 -0
- package/tests/unit/modeling/actions/index.spec.ts +113 -0
- package/tests/unit/modeling/domain_asociation.spec.ts +28 -0
- package/tests/unit/modeling/domain_entity_parents.spec.ts +49 -0
- package/tests/unit/modeling/exposed_entity_actions.spec.ts +47 -0
- package/tests/unit/models/store/Deployment.spec.ts +108 -44
package/src/sdk/Sdk.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { OrganizationsSdk } from './OrganizationsSdk.js'
|
|
|
13
13
|
import { DataCatalogSdk } from './DataCatalogSdk.js'
|
|
14
14
|
import { GroupsSdk } from './GroupsSdk.js'
|
|
15
15
|
import { AiSdk } from './AiSdk.js'
|
|
16
|
+
import { DeploymentsSdk } from './DeploymentsSdk.js'
|
|
16
17
|
|
|
17
18
|
const baseUriSymbol = Symbol('baseUri')
|
|
18
19
|
|
|
@@ -84,6 +85,8 @@ export abstract class Sdk {
|
|
|
84
85
|
groups = new GroupsSdk(this)
|
|
85
86
|
|
|
86
87
|
ai = new AiSdk(this)
|
|
88
|
+
|
|
89
|
+
deployments = new DeploymentsSdk(this)
|
|
87
90
|
/**
|
|
88
91
|
* When set it limits log output to minimum.
|
|
89
92
|
*/
|
package/src/sdk/SdkBase.ts
CHANGED
|
@@ -208,7 +208,7 @@ export class SdkBase {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
protected readResponseBody(response: IStoreResponse, errorPrefix: string): string {
|
|
211
|
-
if (response.status
|
|
211
|
+
if (![200, 202].includes(response.status)) {
|
|
212
212
|
this.logInvalidResponse(response)
|
|
213
213
|
throw this.createApiError(`${errorPrefix}${E_RESPONSE_STATUS}${response.status}`, response.body)
|
|
214
214
|
}
|
|
@@ -221,9 +221,15 @@ export class SdkBase {
|
|
|
221
221
|
return response.body
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
/**
|
|
225
|
+
* A helper function that asserts the `body` is set and it is a valid JSON.
|
|
226
|
+
* @param response The response that is expected to have a valid JSON on the `body`
|
|
227
|
+
* @param errorPrefix The error prefix for the errors thrown
|
|
228
|
+
* @returns The valid JSON response object
|
|
229
|
+
*/
|
|
230
|
+
protected readResponseJSON<T = unknown>(response: IStoreResponse, errorPrefix: string): T {
|
|
225
231
|
const body = this.readResponseBody(response, errorPrefix)
|
|
226
|
-
let data:
|
|
232
|
+
let data: T
|
|
227
233
|
try {
|
|
228
234
|
data = JSON.parse(body)
|
|
229
235
|
} catch {
|
|
@@ -231,4 +237,30 @@ export class SdkBase {
|
|
|
231
237
|
}
|
|
232
238
|
return data
|
|
233
239
|
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* A helper function to process List response.
|
|
243
|
+
* The list response is standardised response containing the `items` and `cursor` properties.
|
|
244
|
+
* The list response also returns 200 status code. The logic validates the JSON response,
|
|
245
|
+
* 200 status code, and whether the response the `items` array (can be empty).
|
|
246
|
+
*
|
|
247
|
+
* @param result The read results of a standard List request
|
|
248
|
+
* @param prefix The error prefix to use with the error codes.
|
|
249
|
+
* @returns The valid list results.
|
|
250
|
+
*/
|
|
251
|
+
protected processListResponse<T = unknown>(result: IStoreResponse, prefix: string): ContextListResult<T> {
|
|
252
|
+
this.inspectCommonStatusCodes(result)
|
|
253
|
+
// Status check happens in the readResponseBody method.
|
|
254
|
+
const data = this.readResponseJSON<ContextListResult<T>>(result, prefix)
|
|
255
|
+
if (!Array.isArray(data.items)) {
|
|
256
|
+
throw new Exception(`${prefix}${E_RESPONSE_UNKNOWN}`, { code: 'E_RESPONSE_UNKNOWN', status: result.status })
|
|
257
|
+
}
|
|
258
|
+
return data
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
protected assertObjectKind(prefix: string, expected: string, actual?: unknown): void {
|
|
262
|
+
if (expected !== actual) {
|
|
263
|
+
throw new Exception(`${prefix}${E_RESPONSE_UNKNOWN}`, { code: 'E_RESPONSE_UNKNOWN', status: 500 })
|
|
264
|
+
}
|
|
265
|
+
}
|
|
234
266
|
}
|
package/src/sdk/SdkMock.ts
CHANGED
|
@@ -27,6 +27,9 @@ import type { ForeignDomainDependency } from '../modeling/types.js'
|
|
|
27
27
|
import { nanoid } from '../nanoid.js'
|
|
28
28
|
import type { AiSessionSchema, AiSessionApp } from '../models/AiSession.js'
|
|
29
29
|
import type { AiMessageSchema } from '../models/AiMessage.js'
|
|
30
|
+
import type { DeploymentSchema } from '../models/store/Deployment.js'
|
|
31
|
+
import { DeploymentEnvironment } from '../models/store/Deployment.js'
|
|
32
|
+
import type { CreateDeploymentPayload } from './DeploymentsSdk.js'
|
|
30
33
|
|
|
31
34
|
export interface MockResult {
|
|
32
35
|
/**
|
|
@@ -1614,6 +1617,106 @@ export class SdkMock {
|
|
|
1614
1617
|
},
|
|
1615
1618
|
}
|
|
1616
1619
|
|
|
1620
|
+
/**
|
|
1621
|
+
* Deployments API mocks.
|
|
1622
|
+
*/
|
|
1623
|
+
deployments = {
|
|
1624
|
+
list: async (init?: MockListResult, options?: InterceptOptions): Promise<void> => {
|
|
1625
|
+
const { mock } = this
|
|
1626
|
+
const respond = this.createDefaultResponse(
|
|
1627
|
+
200,
|
|
1628
|
+
{ 'content-type': 'application/json' },
|
|
1629
|
+
() => {
|
|
1630
|
+
const obj: ContextListResult<DeploymentSchema> = {
|
|
1631
|
+
items: this.gen.deployments.deployments(init?.size ?? 5),
|
|
1632
|
+
cursor: this.createCursorOption(init),
|
|
1633
|
+
}
|
|
1634
|
+
return JSON.stringify(obj)
|
|
1635
|
+
},
|
|
1636
|
+
init
|
|
1637
|
+
)
|
|
1638
|
+
await mock.add(
|
|
1639
|
+
{
|
|
1640
|
+
match: {
|
|
1641
|
+
uri: RouteBuilder.deployments(':oid'),
|
|
1642
|
+
methods: ['GET'],
|
|
1643
|
+
},
|
|
1644
|
+
respond,
|
|
1645
|
+
},
|
|
1646
|
+
options
|
|
1647
|
+
)
|
|
1648
|
+
},
|
|
1649
|
+
create: async (init?: MockResult, options?: InterceptOptions): Promise<void> => {
|
|
1650
|
+
const { mock } = this
|
|
1651
|
+
const respond = this.createDefaultResponse(
|
|
1652
|
+
202,
|
|
1653
|
+
{ 'content-type': 'application/json' },
|
|
1654
|
+
(req: SerializedRequest) => {
|
|
1655
|
+
const raw = req.body as string
|
|
1656
|
+
const payload = JSON.parse(raw) as CreateDeploymentPayload
|
|
1657
|
+
const obj = this.gen.deployments.deployment({
|
|
1658
|
+
orgId: req.params.oid,
|
|
1659
|
+
apiId: payload.fid,
|
|
1660
|
+
modelVersion: payload.modelVersion,
|
|
1661
|
+
env: payload.env,
|
|
1662
|
+
...(payload.env === DeploymentEnvironment.PROD ? { version: payload.version } : {}),
|
|
1663
|
+
})
|
|
1664
|
+
return JSON.stringify(obj)
|
|
1665
|
+
},
|
|
1666
|
+
init
|
|
1667
|
+
)
|
|
1668
|
+
await mock.add(
|
|
1669
|
+
{
|
|
1670
|
+
match: {
|
|
1671
|
+
uri: RouteBuilder.deployments(':oid'),
|
|
1672
|
+
methods: ['POST'],
|
|
1673
|
+
},
|
|
1674
|
+
respond,
|
|
1675
|
+
},
|
|
1676
|
+
options
|
|
1677
|
+
)
|
|
1678
|
+
},
|
|
1679
|
+
read: async (init?: MockResult, options?: InterceptOptions): Promise<void> => {
|
|
1680
|
+
const { mock } = this
|
|
1681
|
+
const respond = this.createDefaultResponse(
|
|
1682
|
+
200,
|
|
1683
|
+
{ 'content-type': 'application/json' },
|
|
1684
|
+
(req: SerializedRequest) => {
|
|
1685
|
+
const obj = this.gen.deployments.deployment({
|
|
1686
|
+
key: req.params.id,
|
|
1687
|
+
orgId: req.params.oid,
|
|
1688
|
+
})
|
|
1689
|
+
return JSON.stringify(obj)
|
|
1690
|
+
},
|
|
1691
|
+
init
|
|
1692
|
+
)
|
|
1693
|
+
await mock.add(
|
|
1694
|
+
{
|
|
1695
|
+
match: {
|
|
1696
|
+
uri: RouteBuilder.deployment(':oid', ':id'),
|
|
1697
|
+
methods: ['GET'],
|
|
1698
|
+
},
|
|
1699
|
+
respond,
|
|
1700
|
+
},
|
|
1701
|
+
options
|
|
1702
|
+
)
|
|
1703
|
+
},
|
|
1704
|
+
deactivate: async (init?: MockResult, options?: InterceptOptions): Promise<void> => {
|
|
1705
|
+
const { mock } = this
|
|
1706
|
+
const respond = this.createDefaultResponse(204, undefined, undefined, init)
|
|
1707
|
+
await mock.add(
|
|
1708
|
+
{
|
|
1709
|
+
match: {
|
|
1710
|
+
uri: RouteBuilder.deployment(':oid', ':id'),
|
|
1711
|
+
methods: ['DELETE'],
|
|
1712
|
+
},
|
|
1713
|
+
respond,
|
|
1714
|
+
},
|
|
1715
|
+
options
|
|
1716
|
+
)
|
|
1717
|
+
},
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1617
1720
|
/**
|
|
1618
1721
|
* Trash Data Catalog mocks.
|
|
1619
1722
|
*/
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { ApiModel } from '../../../src/modeling/ApiModel.js'
|
|
3
|
+
import { RuntimeApiModel, type RuntimeApiModelSchema } from '../../../src/modeling/RuntimeApiModel.js'
|
|
4
|
+
import { DataDomain } from '../../../src/modeling/DataDomain.js'
|
|
5
|
+
|
|
6
|
+
test.group('RuntimeApiModel', () => {
|
|
7
|
+
test('initializes from schema', ({ assert }) => {
|
|
8
|
+
const domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
9
|
+
const model = domain.addModel({ info: { name: 'Test Model' } })
|
|
10
|
+
const entity = model.addEntity({ key: 'ent-1', info: { name: 'User' } })
|
|
11
|
+
|
|
12
|
+
const baseModel = new ApiModel(
|
|
13
|
+
{
|
|
14
|
+
key: 'api-1',
|
|
15
|
+
info: { name: 'Test API' },
|
|
16
|
+
},
|
|
17
|
+
domain
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const expose = baseModel.exposeEntity({ key: entity.key })
|
|
21
|
+
expose.setResourcePath('/users/{id}')
|
|
22
|
+
expose.addActionFromKind('read')
|
|
23
|
+
|
|
24
|
+
const schema: RuntimeApiModelSchema = {
|
|
25
|
+
...baseModel.toJSON(),
|
|
26
|
+
routingMap: {
|
|
27
|
+
GET: [{ path: '/users/{id}', lookup: { exposedEntityKey: expose.key, actionKind: 'read' } }],
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const runtimeModel = new RuntimeApiModel(schema, domain.toJSON())
|
|
32
|
+
assert.deepEqual(runtimeModel.toJSON().routingMap, schema.routingMap)
|
|
33
|
+
}).tags(['@modeling', '@runtime'])
|
|
34
|
+
|
|
35
|
+
test('resolves path with matchit', ({ assert }) => {
|
|
36
|
+
const domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
37
|
+
const schema = {
|
|
38
|
+
key: 'api-1',
|
|
39
|
+
info: { name: 'Test API' },
|
|
40
|
+
exposes: [
|
|
41
|
+
{
|
|
42
|
+
key: 'expose-1',
|
|
43
|
+
entity: { key: 'ent-1', type: 'association' },
|
|
44
|
+
actions: [
|
|
45
|
+
{ kind: 'read', type: 'crud' },
|
|
46
|
+
{ kind: 'list', type: 'crud' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
routingMap: {
|
|
51
|
+
GET: [
|
|
52
|
+
{ path: '/users/{id}', lookup: { exposedEntityKey: 'expose-1', actionKind: 'read' } },
|
|
53
|
+
{ path: '/users', lookup: { exposedEntityKey: 'expose-1', actionKind: 'list' } },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const runtimeModel = new RuntimeApiModel(schema as any, domain.toJSON())
|
|
59
|
+
|
|
60
|
+
const listResult = runtimeModel.lookupAction('GET', '/users')
|
|
61
|
+
assert.isOk(listResult)
|
|
62
|
+
assert.equal(listResult!.action.kind, 'list')
|
|
63
|
+
assert.deepEqual(listResult!.params, {})
|
|
64
|
+
|
|
65
|
+
const readResult = runtimeModel.lookupAction('get', '/users/123') // Case insensitivity test
|
|
66
|
+
assert.isOk(readResult)
|
|
67
|
+
assert.equal(readResult!.action.kind, 'read')
|
|
68
|
+
assert.deepEqual(readResult!.params, { id: '123' })
|
|
69
|
+
}).tags(['@modeling', '@runtime'])
|
|
70
|
+
|
|
71
|
+
test('returns undefined for unknown routes', ({ assert }) => {
|
|
72
|
+
const domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
73
|
+
const schema = {
|
|
74
|
+
key: 'api-1',
|
|
75
|
+
info: { name: 'Test API' },
|
|
76
|
+
routingMap: {
|
|
77
|
+
GET: [{ path: '/users', lookup: { exposedEntityKey: 'expose-1', actionKind: 'list' } }],
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const runtimeModel = new RuntimeApiModel(schema as any, domain.toJSON())
|
|
82
|
+
|
|
83
|
+
// Method not found
|
|
84
|
+
const postResult = runtimeModel.lookupAction('POST', '/users')
|
|
85
|
+
assert.isUndefined(postResult)
|
|
86
|
+
|
|
87
|
+
// Method found, path not found
|
|
88
|
+
const getResult = runtimeModel.lookupAction('GET', '/unknown')
|
|
89
|
+
assert.isUndefined(getResult)
|
|
90
|
+
}).tags(['@modeling', '@runtime'])
|
|
91
|
+
|
|
92
|
+
test('returns undefined when entity or action is missing', ({ assert }) => {
|
|
93
|
+
const domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
94
|
+
const schema = {
|
|
95
|
+
key: 'api-1',
|
|
96
|
+
info: { name: 'Test API' },
|
|
97
|
+
exposes: [
|
|
98
|
+
{
|
|
99
|
+
key: 'expose-1',
|
|
100
|
+
entity: { key: 'ent-1', type: 'association' },
|
|
101
|
+
actions: [{ kind: 'list', type: 'crud' }],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
routingMap: {
|
|
105
|
+
GET: [
|
|
106
|
+
{ path: '/missing-entity', lookup: { exposedEntityKey: 'missing-expose', actionKind: 'list' } },
|
|
107
|
+
{ path: '/missing-action', lookup: { exposedEntityKey: 'expose-1', actionKind: 'read' } },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const runtimeModel = new RuntimeApiModel(schema as any, domain.toJSON())
|
|
113
|
+
|
|
114
|
+
// Exposed entity not found
|
|
115
|
+
const missingEntityResult = runtimeModel.lookupAction('GET', '/missing-entity')
|
|
116
|
+
assert.isUndefined(missingEntityResult)
|
|
117
|
+
|
|
118
|
+
// Action not found on entity
|
|
119
|
+
const missingActionResult = runtimeModel.lookupAction('GET', '/missing-action')
|
|
120
|
+
assert.isUndefined(missingActionResult)
|
|
121
|
+
}).tags(['@modeling', '@runtime'])
|
|
122
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import { createActionFromKind, restoreAction, type ActionKind } from '../../../../src/modeling/actions/index.js'
|
|
3
|
+
import { ListAction } from '../../../../src/modeling/actions/ListAction.js'
|
|
4
|
+
import { ReadAction } from '../../../../src/modeling/actions/ReadAction.js'
|
|
5
|
+
import { CreateAction } from '../../../../src/modeling/actions/CreateAction.js'
|
|
6
|
+
import { UpdateAction } from '../../../../src/modeling/actions/UpdateAction.js'
|
|
7
|
+
import { DeleteAction } from '../../../../src/modeling/actions/DeleteAction.js'
|
|
8
|
+
import { SearchAction } from '../../../../src/modeling/actions/SearchAction.js'
|
|
9
|
+
import { DataDomain } from '../../../../src/modeling/DataDomain.js'
|
|
10
|
+
import { ApiModel } from '../../../../src/modeling/ApiModel.js'
|
|
11
|
+
import type { ExposedEntity } from '../../../../src/modeling/ExposedEntity.js'
|
|
12
|
+
|
|
13
|
+
test.group('modeling/actions/index', (group) => {
|
|
14
|
+
let domain: DataDomain
|
|
15
|
+
let baseModel: ApiModel
|
|
16
|
+
let expose: ExposedEntity
|
|
17
|
+
|
|
18
|
+
group.each.setup(() => {
|
|
19
|
+
domain = new DataDomain({ info: { version: '1.0.0' } })
|
|
20
|
+
const model = domain.addModel({ info: { name: 'Test Model' } })
|
|
21
|
+
const entity = model.addEntity({ key: 'ent-1', info: { name: 'User' } })
|
|
22
|
+
|
|
23
|
+
baseModel = new ApiModel(
|
|
24
|
+
{
|
|
25
|
+
key: 'api-1',
|
|
26
|
+
info: { name: 'Test API' },
|
|
27
|
+
},
|
|
28
|
+
domain
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
expose = baseModel.exposeEntity({ key: entity.key })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('createActionFromKind: creates ListAction', ({ assert }) => {
|
|
35
|
+
const action = createActionFromKind(expose, 'list')
|
|
36
|
+
assert.instanceOf(action, ListAction)
|
|
37
|
+
assert.equal(action.kind, 'list')
|
|
38
|
+
}).tags(['@actions'])
|
|
39
|
+
|
|
40
|
+
test('createActionFromKind: creates ReadAction', ({ assert }) => {
|
|
41
|
+
const action = createActionFromKind(expose, 'read')
|
|
42
|
+
assert.instanceOf(action, ReadAction)
|
|
43
|
+
assert.equal(action.kind, 'read')
|
|
44
|
+
}).tags(['@actions'])
|
|
45
|
+
|
|
46
|
+
test('createActionFromKind: creates CreateAction', ({ assert }) => {
|
|
47
|
+
const action = createActionFromKind(expose, 'create')
|
|
48
|
+
assert.instanceOf(action, CreateAction)
|
|
49
|
+
assert.equal(action.kind, 'create')
|
|
50
|
+
}).tags(['@actions'])
|
|
51
|
+
|
|
52
|
+
test('createActionFromKind: creates UpdateAction', ({ assert }) => {
|
|
53
|
+
const action = createActionFromKind(expose, 'update')
|
|
54
|
+
assert.instanceOf(action, UpdateAction)
|
|
55
|
+
assert.equal(action.kind, 'update')
|
|
56
|
+
}).tags(['@actions'])
|
|
57
|
+
|
|
58
|
+
test('createActionFromKind: creates DeleteAction', ({ assert }) => {
|
|
59
|
+
const action = createActionFromKind(expose, 'delete')
|
|
60
|
+
assert.instanceOf(action, DeleteAction)
|
|
61
|
+
assert.equal(action.kind, 'delete')
|
|
62
|
+
}).tags(['@actions'])
|
|
63
|
+
|
|
64
|
+
test('createActionFromKind: creates SearchAction', ({ assert }) => {
|
|
65
|
+
const action = createActionFromKind(expose, 'search')
|
|
66
|
+
assert.instanceOf(action, SearchAction)
|
|
67
|
+
assert.equal(action.kind, 'search')
|
|
68
|
+
}).tags(['@actions'])
|
|
69
|
+
|
|
70
|
+
test('createActionFromKind: throws exception for unknown action kind', ({ assert }) => {
|
|
71
|
+
assert.throws(() => createActionFromKind(expose, 'unknown' as ActionKind), 'Unknown action kind')
|
|
72
|
+
}).tags(['@actions'])
|
|
73
|
+
|
|
74
|
+
test('restoreAction: restores ListAction', ({ assert }) => {
|
|
75
|
+
const action = restoreAction(expose, { kind: 'list' })
|
|
76
|
+
assert.instanceOf(action, ListAction)
|
|
77
|
+
assert.equal(action.kind, 'list')
|
|
78
|
+
}).tags(['@actions'])
|
|
79
|
+
|
|
80
|
+
test('restoreAction: restores ReadAction', ({ assert }) => {
|
|
81
|
+
const action = restoreAction(expose, { kind: 'read' })
|
|
82
|
+
assert.instanceOf(action, ReadAction)
|
|
83
|
+
assert.equal(action.kind, 'read')
|
|
84
|
+
}).tags(['@actions'])
|
|
85
|
+
|
|
86
|
+
test('restoreAction: restores CreateAction', ({ assert }) => {
|
|
87
|
+
const action = restoreAction(expose, { kind: 'create' })
|
|
88
|
+
assert.instanceOf(action, CreateAction)
|
|
89
|
+
assert.equal(action.kind, 'create')
|
|
90
|
+
}).tags(['@actions'])
|
|
91
|
+
|
|
92
|
+
test('restoreAction: restores UpdateAction', ({ assert }) => {
|
|
93
|
+
const action = restoreAction(expose, { kind: 'update' })
|
|
94
|
+
assert.instanceOf(action, UpdateAction)
|
|
95
|
+
assert.equal(action.kind, 'update')
|
|
96
|
+
}).tags(['@actions'])
|
|
97
|
+
|
|
98
|
+
test('restoreAction: restores DeleteAction', ({ assert }) => {
|
|
99
|
+
const action = restoreAction(expose, { kind: 'delete' })
|
|
100
|
+
assert.instanceOf(action, DeleteAction)
|
|
101
|
+
assert.equal(action.kind, 'delete')
|
|
102
|
+
}).tags(['@actions'])
|
|
103
|
+
|
|
104
|
+
test('restoreAction: restores SearchAction', ({ assert }) => {
|
|
105
|
+
const action = restoreAction(expose, { kind: 'search' })
|
|
106
|
+
assert.instanceOf(action, SearchAction)
|
|
107
|
+
assert.equal(action.kind, 'search')
|
|
108
|
+
}).tags(['@actions'])
|
|
109
|
+
|
|
110
|
+
test('restoreAction: throws exception for unknown action kind', ({ assert }) => {
|
|
111
|
+
assert.throws(() => restoreAction(expose, { kind: 'unknown' }), 'Unknown action kind')
|
|
112
|
+
}).tags(['@actions'])
|
|
113
|
+
})
|
|
@@ -794,6 +794,34 @@ test.group('DomainAssociation.readBinding()', () => {
|
|
|
794
794
|
})
|
|
795
795
|
})
|
|
796
796
|
|
|
797
|
+
test.group('DomainAssociation.readWebBinding()', () => {
|
|
798
|
+
test('returns undefined if no web binding exists', ({ assert }) => {
|
|
799
|
+
const dataDomain = new DataDomain()
|
|
800
|
+
const association = new DomainAssociation(dataDomain, 'test-parent')
|
|
801
|
+
const webBindings = association.readWebBinding()
|
|
802
|
+
assert.isUndefined(webBindings)
|
|
803
|
+
}).tags(['@modeling', '@association'])
|
|
804
|
+
|
|
805
|
+
test('returns the web binding schema if it exists', ({ assert }) => {
|
|
806
|
+
const dataDomain = new DataDomain()
|
|
807
|
+
const association = new DomainAssociation(dataDomain, 'test-parent', {
|
|
808
|
+
bindings: [{ type: 'web', schema: { hidden: true } }],
|
|
809
|
+
})
|
|
810
|
+
const webBindings = association.readWebBinding()
|
|
811
|
+
assert.deepEqual(webBindings, { hidden: true })
|
|
812
|
+
}).tags(['@modeling', '@association'])
|
|
813
|
+
|
|
814
|
+
test('returns undefined if the web binding exists but has no schema', ({ assert }) => {
|
|
815
|
+
const dataDomain = new DataDomain()
|
|
816
|
+
const association = new DomainAssociation(dataDomain, 'test-parent', {
|
|
817
|
+
// @ts-expect-error Testing undefined schema
|
|
818
|
+
bindings: [{ type: 'web', schema: undefined }],
|
|
819
|
+
})
|
|
820
|
+
const webBindings = association.readWebBinding()
|
|
821
|
+
assert.isUndefined(webBindings)
|
|
822
|
+
}).tags(['@modeling', '@association'])
|
|
823
|
+
})
|
|
824
|
+
|
|
797
825
|
test.group('DomainAssociation.addSemantic()', () => {
|
|
798
826
|
test('adds a new semantic to the association', ({ assert }) => {
|
|
799
827
|
const dataDomain = new DataDomain()
|
|
@@ -59,6 +59,55 @@ test.group('DomainEntity.listParents()', () => {
|
|
|
59
59
|
})
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
+
test.group('DomainEntity.listAllParents()', () => {
|
|
63
|
+
test('lists all parent entities recursively upwards (default)', ({ assert }) => {
|
|
64
|
+
const dataDomain = new DataDomain()
|
|
65
|
+
const model = dataDomain.addModel()
|
|
66
|
+
const grandparent = model.addEntity({ key: 'grandparent' })
|
|
67
|
+
const parent = model.addEntity({ key: 'parent' })
|
|
68
|
+
const child = model.addEntity({ key: 'child' })
|
|
69
|
+
parent.addParent(grandparent.key)
|
|
70
|
+
child.addParent(parent.key)
|
|
71
|
+
const parents = child.listAllParents()
|
|
72
|
+
assert.deepEqual(parents, [parent, grandparent])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('lists all parent entities recursively downwards', ({ assert }) => {
|
|
76
|
+
const dataDomain = new DataDomain()
|
|
77
|
+
const model = dataDomain.addModel()
|
|
78
|
+
const grandparent = model.addEntity({ key: 'grandparent' })
|
|
79
|
+
const parent = model.addEntity({ key: 'parent' })
|
|
80
|
+
const child = model.addEntity({ key: 'child' })
|
|
81
|
+
parent.addParent(grandparent.key)
|
|
82
|
+
child.addParent(parent.key)
|
|
83
|
+
const parents = child.listAllParents('down')
|
|
84
|
+
assert.deepEqual(parents, [grandparent, parent])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('handles multiple inheritance properly', ({ assert }) => {
|
|
88
|
+
const dataDomain = new DataDomain()
|
|
89
|
+
const model = dataDomain.addModel()
|
|
90
|
+
const grandparent1 = model.addEntity({ key: 'grandparent1' })
|
|
91
|
+
const grandparent2 = model.addEntity({ key: 'grandparent2' })
|
|
92
|
+
const parent1 = model.addEntity({ key: 'parent1' })
|
|
93
|
+
const parent2 = model.addEntity({ key: 'parent2' })
|
|
94
|
+
const child = model.addEntity({ key: 'child' })
|
|
95
|
+
|
|
96
|
+
parent1.addParent(grandparent1.key)
|
|
97
|
+
parent2.addParent(grandparent2.key)
|
|
98
|
+
child.addParent(parent1.key)
|
|
99
|
+
child.addParent(parent2.key)
|
|
100
|
+
|
|
101
|
+
const parentsUp = child.listAllParents('up')
|
|
102
|
+
// Order based on BFS: parent1, parent2, grandparent1, grandparent2
|
|
103
|
+
assert.deepEqual(parentsUp, [parent1, parent2, grandparent1, grandparent2])
|
|
104
|
+
|
|
105
|
+
const parentsDown = child.listAllParents('down')
|
|
106
|
+
// Order reversed: grandparent2, grandparent1, parent2, parent1
|
|
107
|
+
assert.deepEqual(parentsDown, [grandparent2, grandparent1, parent2, parent1])
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
62
111
|
test.group('DomainEntity.addParent()', () => {
|
|
63
112
|
test('adds a parent to the entity', ({ assert }) => {
|
|
64
113
|
const dataDomain = new DataDomain()
|
|
@@ -198,4 +198,51 @@ test.group('ExposedEntity::actions', (group) => {
|
|
|
198
198
|
assert.equal(restoredAction.kind, 'search', 'Action kind should be search')
|
|
199
199
|
assert.instanceOf(restoredAction, SearchAction, 'Action should be an instance of SearchAction')
|
|
200
200
|
}).tags(['@modeling', '@action', '@restoring'])
|
|
201
|
+
|
|
202
|
+
test('addActionFromKind adds a new action', ({ assert }) => {
|
|
203
|
+
const model = new ApiModel(
|
|
204
|
+
{
|
|
205
|
+
exposes: [
|
|
206
|
+
{
|
|
207
|
+
kind: ExposedEntityKind,
|
|
208
|
+
key: nanoid(),
|
|
209
|
+
entity: { key: entity.key },
|
|
210
|
+
isRoot: true,
|
|
211
|
+
hasCollection: false,
|
|
212
|
+
resourcePath: '',
|
|
213
|
+
actions: [],
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
domain
|
|
218
|
+
)
|
|
219
|
+
const expose = Array.from(model.exposes.values())[0]!
|
|
220
|
+
const action = expose.addActionFromKind('list')
|
|
221
|
+
assert.instanceOf(action, ListAction)
|
|
222
|
+
assert.equal(action.kind, 'list')
|
|
223
|
+
assert.lengthOf(expose.actions, 1)
|
|
224
|
+
assert.strictEqual(expose.actions[0], action)
|
|
225
|
+
}).tags(['@modeling', '@action'])
|
|
226
|
+
|
|
227
|
+
test('addActionFromKind throws if action already exists', ({ assert }) => {
|
|
228
|
+
const model = new ApiModel(
|
|
229
|
+
{
|
|
230
|
+
exposes: [
|
|
231
|
+
{
|
|
232
|
+
kind: ExposedEntityKind,
|
|
233
|
+
key: nanoid(),
|
|
234
|
+
entity: { key: entity.key },
|
|
235
|
+
isRoot: true,
|
|
236
|
+
hasCollection: false,
|
|
237
|
+
resourcePath: '',
|
|
238
|
+
actions: [],
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
domain
|
|
243
|
+
)
|
|
244
|
+
const expose = Array.from(model.exposes.values())[0]!
|
|
245
|
+
expose.addActionFromKind('list')
|
|
246
|
+
assert.throws(() => expose.addActionFromKind('list'), 'Action of kind "list" already exists for this exposure')
|
|
247
|
+
}).tags(['@modeling', '@action'])
|
|
201
248
|
})
|