@api-client/core 0.19.23 → 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.
Files changed (118) hide show
  1. package/build/src/browser.d.ts +3 -0
  2. package/build/src/browser.d.ts.map +1 -1
  3. package/build/src/browser.js +2 -0
  4. package/build/src/browser.js.map +1 -1
  5. package/build/src/index.d.ts +5 -0
  6. package/build/src/index.d.ts.map +1 -1
  7. package/build/src/index.js +4 -0
  8. package/build/src/index.js.map +1 -1
  9. package/build/src/mocking/ModelingMock.d.ts +2 -0
  10. package/build/src/mocking/ModelingMock.d.ts.map +1 -1
  11. package/build/src/mocking/ModelingMock.js +2 -0
  12. package/build/src/mocking/ModelingMock.js.map +1 -1
  13. package/build/src/mocking/lib/Deployment.d.ts +16 -0
  14. package/build/src/mocking/lib/Deployment.d.ts.map +1 -0
  15. package/build/src/mocking/lib/Deployment.js +76 -0
  16. package/build/src/mocking/lib/Deployment.js.map +1 -0
  17. package/build/src/modeling/Bindings.d.ts +4 -0
  18. package/build/src/modeling/Bindings.d.ts.map +1 -1
  19. package/build/src/modeling/Bindings.js.map +1 -1
  20. package/build/src/modeling/DataFormat.d.ts +1 -1
  21. package/build/src/modeling/DataFormat.d.ts.map +1 -1
  22. package/build/src/modeling/DataFormat.js +2 -0
  23. package/build/src/modeling/DataFormat.js.map +1 -1
  24. package/build/src/modeling/DomainAssociation.d.ts +7 -0
  25. package/build/src/modeling/DomainAssociation.d.ts.map +1 -1
  26. package/build/src/modeling/DomainAssociation.js +10 -0
  27. package/build/src/modeling/DomainAssociation.js.map +1 -1
  28. package/build/src/modeling/DomainEntity.d.ts +9 -1
  29. package/build/src/modeling/DomainEntity.d.ts.map +1 -1
  30. package/build/src/modeling/DomainEntity.js +26 -1
  31. package/build/src/modeling/DomainEntity.js.map +1 -1
  32. package/build/src/modeling/ExposedEntity.d.ts +12 -1
  33. package/build/src/modeling/ExposedEntity.d.ts.map +1 -1
  34. package/build/src/modeling/ExposedEntity.js +24 -1
  35. package/build/src/modeling/ExposedEntity.js.map +1 -1
  36. package/build/src/modeling/RuntimeApiModel.d.ts +52 -0
  37. package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -0
  38. package/build/src/modeling/RuntimeApiModel.js +85 -0
  39. package/build/src/modeling/RuntimeApiModel.js.map +1 -0
  40. package/build/src/modeling/actions/index.d.ts +10 -0
  41. package/build/src/modeling/actions/index.d.ts.map +1 -1
  42. package/build/src/modeling/actions/index.js +30 -0
  43. package/build/src/modeling/actions/index.js.map +1 -1
  44. package/build/src/modeling/index.d.ts.map +1 -1
  45. package/build/src/modeling/index.js +1 -0
  46. package/build/src/modeling/index.js.map +1 -1
  47. package/build/src/modeling/types.d.ts +25 -1
  48. package/build/src/modeling/types.d.ts.map +1 -1
  49. package/build/src/modeling/types.js.map +1 -1
  50. package/build/src/models/kinds.d.ts +1 -0
  51. package/build/src/models/kinds.d.ts.map +1 -1
  52. package/build/src/models/kinds.js +1 -0
  53. package/build/src/models/kinds.js.map +1 -1
  54. package/build/src/models/store/CustomDomain.d.ts +50 -0
  55. package/build/src/models/store/CustomDomain.d.ts.map +1 -0
  56. package/build/src/models/store/CustomDomain.js +79 -0
  57. package/build/src/models/store/CustomDomain.js.map +1 -0
  58. package/build/src/models/store/Deployment.d.ts +118 -0
  59. package/build/src/models/store/Deployment.d.ts.map +1 -0
  60. package/build/src/models/store/Deployment.js +182 -0
  61. package/build/src/models/store/Deployment.js.map +1 -0
  62. package/build/src/models/store/DeploymentCustomDomain.d.ts +52 -0
  63. package/build/src/models/store/DeploymentCustomDomain.d.ts.map +1 -0
  64. package/build/src/models/store/DeploymentCustomDomain.js +84 -0
  65. package/build/src/models/store/DeploymentCustomDomain.js.map +1 -0
  66. package/build/src/sdk/DataCatalogSdk.d.ts.map +1 -1
  67. package/build/src/sdk/DataCatalogSdk.js +22 -179
  68. package/build/src/sdk/DataCatalogSdk.js.map +1 -1
  69. package/build/src/sdk/DeploymentsSdk.d.ts +48 -0
  70. package/build/src/sdk/DeploymentsSdk.d.ts.map +1 -0
  71. package/build/src/sdk/DeploymentsSdk.js +94 -0
  72. package/build/src/sdk/DeploymentsSdk.js.map +1 -0
  73. package/build/src/sdk/RouteBuilder.d.ts +2 -0
  74. package/build/src/sdk/RouteBuilder.d.ts.map +1 -1
  75. package/build/src/sdk/RouteBuilder.js +6 -0
  76. package/build/src/sdk/RouteBuilder.js.map +1 -1
  77. package/build/src/sdk/Sdk.d.ts +2 -0
  78. package/build/src/sdk/Sdk.d.ts.map +1 -1
  79. package/build/src/sdk/Sdk.js +2 -0
  80. package/build/src/sdk/Sdk.js.map +1 -1
  81. package/build/src/sdk/SdkBase.d.ts +19 -1
  82. package/build/src/sdk/SdkBase.d.ts.map +1 -1
  83. package/build/src/sdk/SdkBase.js +31 -1
  84. package/build/src/sdk/SdkBase.js.map +1 -1
  85. package/build/src/sdk/SdkMock.d.ts +9 -0
  86. package/build/src/sdk/SdkMock.d.ts.map +1 -1
  87. package/build/src/sdk/SdkMock.js +73 -0
  88. package/build/src/sdk/SdkMock.js.map +1 -1
  89. package/build/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +2 -1
  91. package/src/matchit.d.ts +19 -0
  92. package/src/mocking/ModelingMock.ts +2 -0
  93. package/src/mocking/lib/Deployment.ts +88 -0
  94. package/src/modeling/Bindings.ts +4 -0
  95. package/src/modeling/DataFormat.ts +4 -0
  96. package/src/modeling/DomainAssociation.ts +11 -0
  97. package/src/modeling/DomainEntity.ts +30 -1
  98. package/src/modeling/ExposedEntity.ts +26 -1
  99. package/src/modeling/RuntimeApiModel.ts +137 -0
  100. package/src/modeling/types.ts +26 -1
  101. package/src/models/kinds.ts +1 -0
  102. package/src/models/store/CustomDomain.ts +119 -0
  103. package/src/models/store/Deployment.ts +250 -0
  104. package/src/models/store/DeploymentCustomDomain.ts +120 -0
  105. package/src/sdk/DataCatalogSdk.ts +22 -176
  106. package/src/sdk/DeploymentsSdk.ts +123 -0
  107. package/src/sdk/RouteBuilder.ts +8 -0
  108. package/src/sdk/Sdk.ts +3 -0
  109. package/src/sdk/SdkBase.ts +35 -3
  110. package/src/sdk/SdkMock.ts +103 -0
  111. package/tests/unit/modeling/RuntimeApiModel.spec.ts +122 -0
  112. package/tests/unit/modeling/actions/index.spec.ts +113 -0
  113. package/tests/unit/modeling/domain_asociation.spec.ts +28 -0
  114. package/tests/unit/modeling/domain_entity_parents.spec.ts +49 -0
  115. package/tests/unit/modeling/exposed_entity_actions.spec.ts +47 -0
  116. package/tests/unit/models/store/CustomDomain.spec.ts +111 -0
  117. package/tests/unit/models/store/Deployment.spec.ts +198 -0
  118. package/tests/unit/models/store/DeploymentCustomDomain.spec.ts +122 -0
@@ -0,0 +1,123 @@
1
+ import { SdkBase, type SdkOptions, E_RESPONSE_STATUS } from './SdkBase.js'
2
+ import { RouteBuilder } from './RouteBuilder.js'
3
+ import { DeploymentEnvironment, DeploymentKind, DeploymentSchema } from '../models/store/Deployment.js'
4
+ import { Exception } from '../exceptions/exception.js'
5
+ import type { ContextListOptions, ContextListResult } from '../events/BaseEvents.js'
6
+
7
+ export interface CreateDeploymentPayload {
8
+ /**
9
+ * API file ID
10
+ */
11
+ fid: string
12
+ /**
13
+ * API deployment version (e.g. "v1", "v2")
14
+ * It is optional for non production environments (e.g. 'dev').
15
+ */
16
+ version?: string
17
+ /**
18
+ * API model version (e.g. "3.1.2", "4.0.0")
19
+ */
20
+ modelVersion: string
21
+ /**
22
+ * Target environment tag (e.g. "prod", "staging", "dev")
23
+ */
24
+ env: DeploymentEnvironment
25
+ }
26
+
27
+ export class DeploymentsSdk extends SdkBase {
28
+ async list(
29
+ oid: string,
30
+ options: ContextListOptions = {},
31
+ request: SdkOptions = {}
32
+ ): Promise<ContextListResult<DeploymentSchema>> {
33
+ const { token = this.sdk.token } = request
34
+ const url = this.sdk.getUrl(RouteBuilder.deployments(oid))
35
+ this.sdk.appendListOptions(url, options)
36
+ const result = await this.sdk.http.get(url.toString(), { token })
37
+ return this.processListResponse<DeploymentSchema>(result, 'Unable to list deployments. ')
38
+ }
39
+
40
+ /**
41
+ * Triggers the creation of a new deployment.
42
+ *
43
+ * @param oid The parent organization key
44
+ * @param payload The deployment payload
45
+ * @param request Optional SDK options
46
+ */
47
+ async create(oid: string, payload: CreateDeploymentPayload, request: SdkOptions = {}): Promise<DeploymentSchema> {
48
+ // Let's do some basic validation before hitting the API endpoint.
49
+ if (payload.env === DeploymentEnvironment.PROD && !payload.version) {
50
+ throw new Exception(`Version must not be empty for production environment: ${payload.env}`, {
51
+ code: 'INVALID_DEPLOYMENT_VERSION',
52
+ help: 'Please provide a version for production environment',
53
+ status: 400,
54
+ })
55
+ } else if (payload.env !== DeploymentEnvironment.PROD && payload.version) {
56
+ throw new Exception(`Version must not be present for non-production environment: ${payload.env}`, {
57
+ code: 'INVALID_DEPLOYMENT_VERSION',
58
+ help: 'Please remove the version for non-production environment',
59
+ status: 400,
60
+ })
61
+ }
62
+ const url = this.sdk.getUrl(RouteBuilder.deployments(oid))
63
+ const { token = this.sdk.token } = request
64
+ const result = await this.sdk.http.post(url.toString(), {
65
+ token,
66
+ body: JSON.stringify(payload),
67
+ headers: {
68
+ 'content-type': 'application/json',
69
+ },
70
+ })
71
+
72
+ this.inspectCommonStatusCodes(result)
73
+ const E_PREFIX = 'Unable to create a deployment. '
74
+ // It is accepted for async processing.
75
+ if (result.status !== 202) {
76
+ this.logInvalidResponse(result)
77
+ throw this.createApiError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.body)
78
+ }
79
+ const data = this.readResponseJSON<DeploymentSchema>(result, E_PREFIX)
80
+ this.assertObjectKind(E_PREFIX, DeploymentKind, data.kind)
81
+ return data
82
+ }
83
+
84
+ /**
85
+ * Reads the status of an API deployment.
86
+ *
87
+ * @param oid The parent organization key
88
+ * @param did The deployment ID
89
+ * @param request Optional SDK options
90
+ */
91
+ async read(oid: string, did: string, request: SdkOptions = {}): Promise<DeploymentSchema> {
92
+ const url = this.sdk.getUrl(RouteBuilder.deployment(oid, did))
93
+ const { token = this.sdk.token } = request
94
+ const result = await this.sdk.http.get(url.toString(), { token })
95
+
96
+ this.inspectCommonStatusCodes(result)
97
+ const E_PREFIX = 'Unable to read a deployment. '
98
+ if (result.status !== 200) {
99
+ this.logInvalidResponse(result)
100
+ throw this.createApiError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.body)
101
+ }
102
+ const data = this.readResponseJSON<DeploymentSchema>(result, E_PREFIX)
103
+ this.assertObjectKind(E_PREFIX, DeploymentKind, data.kind)
104
+ return data
105
+ }
106
+
107
+ /**
108
+ * Deactivates a deployment. Essentially it disables the version of an API.
109
+ * @param oid The organization ID
110
+ * @param did The deployment ID
111
+ */
112
+ async deactivate(oid: string, did: string, request: SdkOptions = {}): Promise<void> {
113
+ const url = this.sdk.getUrl(RouteBuilder.deployment(oid, did))
114
+ const { token = this.sdk.token } = request
115
+ const result = await this.sdk.http.delete(url.toString(), { token })
116
+ this.inspectCommonStatusCodes(result)
117
+ const E_PREFIX = 'Unable to deactivate a deployment. '
118
+ if (result.status !== 204) {
119
+ this.logInvalidResponse(result)
120
+ throw this.createApiError(`${E_PREFIX}${E_RESPONSE_STATUS}${result.status}`, result.body)
121
+ }
122
+ }
123
+ }
@@ -334,4 +334,12 @@ export class RouteBuilder {
334
334
  static orgSlugValidate(): string {
335
335
  return `/v1/orgs/slugs/validate`
336
336
  }
337
+
338
+ static deployments(oid: string): string {
339
+ return `/v1/orgs/${oid}/deployments`
340
+ }
341
+
342
+ static deployment(oid: string, deploymentId: string): string {
343
+ return `${RouteBuilder.deployments(oid)}/${deploymentId}`
344
+ }
337
345
  }
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
  */
@@ -208,7 +208,7 @@ export class SdkBase {
208
208
  }
209
209
 
210
210
  protected readResponseBody(response: IStoreResponse, errorPrefix: string): string {
211
- if (response.status !== 200) {
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
- protected readResponseJSON(response: IStoreResponse, errorPrefix: string): unknown {
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: unknown
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
  }
@@ -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()