@api-client/core 0.20.8 → 0.20.10
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/mocking/ModelingMock.d.ts +26 -0
- package/build/src/mocking/ModelingMock.d.ts.map +1 -1
- package/build/src/mocking/ModelingMock.js +48 -0
- package/build/src/mocking/ModelingMock.js.map +1 -1
- package/build/src/modeling/RuntimeApiModel.d.ts +43 -0
- package/build/src/modeling/RuntimeApiModel.d.ts.map +1 -1
- package/build/src/modeling/RuntimeApiModel.js +1 -0
- package/build/src/modeling/RuntimeApiModel.js.map +1 -1
- package/build/src/modeling/generators/RuntimeModelGenerator.d.ts.map +1 -1
- package/build/src/modeling/generators/RuntimeModelGenerator.js +58 -8
- package/build/src/modeling/generators/RuntimeModelGenerator.js.map +1 -1
- package/build/src/sdk/SdkMock.d.ts +0 -3
- package/build/src/sdk/SdkMock.d.ts.map +1 -1
- package/build/src/sdk/SdkMock.js +60 -86
- package/build/src/sdk/SdkMock.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/mocking/ModelingMock.ts +54 -0
- package/src/modeling/RuntimeApiModel.ts +45 -0
- package/src/modeling/generators/RuntimeModelGenerator.ts +61 -9
- package/src/sdk/SdkMock.ts +60 -95
- package/tests/fixtures/modeling/runtime/domains.ts +270 -0
- package/tests/unit/modeling/generators/RuntimeModelGenerator.spec.ts +352 -114
package/package.json
CHANGED
|
@@ -10,6 +10,9 @@ import { DataCatalog } from './lib/DataCatalog.js'
|
|
|
10
10
|
import { Permission } from './lib/Permission.js'
|
|
11
11
|
import { Ai } from './lib/Ai.js'
|
|
12
12
|
import { Deployment } from './lib/Deployment.js'
|
|
13
|
+
import type { MockListResult } from '../sdk/SdkMock.js'
|
|
14
|
+
import { ResourceResponse } from '../sdk/SdkBase.js'
|
|
15
|
+
import { ContextListResult } from '../events/BaseEvents.js'
|
|
13
16
|
|
|
14
17
|
export class ModelingMock {
|
|
15
18
|
faker: Faker = faker
|
|
@@ -24,4 +27,55 @@ export class ModelingMock {
|
|
|
24
27
|
permission = new Permission()
|
|
25
28
|
ai = new Ai()
|
|
26
29
|
deployments = new Deployment()
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wraps a single resource in a standard API resource response structure and serializes it to a JSON string.
|
|
33
|
+
*
|
|
34
|
+
* @param resource - The resource object to wrap.
|
|
35
|
+
* @returns A JSON string representation of the wrapped resource.
|
|
36
|
+
*/
|
|
37
|
+
public wrapResource<T extends object>(resource: T): string {
|
|
38
|
+
const wrapper: ResourceResponse<T> = {
|
|
39
|
+
data: resource,
|
|
40
|
+
}
|
|
41
|
+
return JSON.stringify(wrapper)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wraps a list of items in a standard API list response structure, including an optional pagination cursor,
|
|
46
|
+
* and serializes it to a JSON string.
|
|
47
|
+
*
|
|
48
|
+
* @param items - The array of items to wrap.
|
|
49
|
+
* @param init - Optional configuration for the mock list result, such as cursor settings.
|
|
50
|
+
* @returns A JSON string representation of the wrapped list.
|
|
51
|
+
*/
|
|
52
|
+
public wrapList<T extends object>(items: (T | undefined)[], init?: MockListResult): string {
|
|
53
|
+
const obj: ContextListResult<T | undefined> = {
|
|
54
|
+
data: items,
|
|
55
|
+
cursor: this.createCursorOption(init),
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(obj)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generates or determines the pagination cursor based on the provided mock initialization options.
|
|
62
|
+
* If a cursor is explicitly provided, it is returned. If explicitly disabled, it returns undefined.
|
|
63
|
+
* Otherwise, it randomly generates a mock JWT string to simulate a cursor.
|
|
64
|
+
*
|
|
65
|
+
* @param init - Configuration object detailing cursor behavior.
|
|
66
|
+
* @returns The cursor string or undefined if no cursor should be present.
|
|
67
|
+
*/
|
|
68
|
+
protected createCursorOption(init: MockListResult = {}): string | undefined {
|
|
69
|
+
if (init.cursor === false) {
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
if (typeof init.cursor === 'string') {
|
|
73
|
+
return init.cursor
|
|
74
|
+
}
|
|
75
|
+
const hasCursor = init.cursor === true ? true : this.faker.datatype.boolean()
|
|
76
|
+
if (!hasCursor) {
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
return this.faker.internet.jwt()
|
|
80
|
+
}
|
|
27
81
|
}
|
|
@@ -11,12 +11,55 @@ import { SemanticType } from './Semantics.js'
|
|
|
11
11
|
import { AccessRule, AccessRuleExecutionPhase } from './rules/AccessRule.js'
|
|
12
12
|
import { Exception } from '../exceptions/exception.js'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Describes a mapped parameter for a route.
|
|
16
|
+
*/
|
|
17
|
+
export interface RouteParam {
|
|
18
|
+
/**
|
|
19
|
+
* The name of the parameter in the path (e.g. "userId")
|
|
20
|
+
* This can be anything as the user can freely change the param names.
|
|
21
|
+
*/
|
|
22
|
+
paramName: string
|
|
23
|
+
/**
|
|
24
|
+
* The "key" of the exposed entity this parameter belongs to.
|
|
25
|
+
*/
|
|
26
|
+
exposedEntityKey: string
|
|
27
|
+
/**
|
|
28
|
+
* The "name" of the exposed entity this parameter belongs to.
|
|
29
|
+
*/
|
|
30
|
+
exposedEntityName: string
|
|
31
|
+
/**
|
|
32
|
+
* The primary key's property name of the exposed entity (e.g., "id").
|
|
33
|
+
*/
|
|
34
|
+
propertyName: string
|
|
35
|
+
/**
|
|
36
|
+
* The name of the association that connects the parent entity to its child (e.g., "comments")
|
|
37
|
+
* If its missing it means that there's no association, so there's no sub-endpoint in the path
|
|
38
|
+
* (e.g., `/users/123` - It's a single root endpoint - it doesn't have any sub-endpoints.)
|
|
39
|
+
*/
|
|
40
|
+
associationName?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
14
43
|
/**
|
|
15
44
|
* Identifies a specific exposed entity and its action kind.
|
|
16
45
|
*/
|
|
17
46
|
export interface RouteLookup {
|
|
47
|
+
/**
|
|
48
|
+
* The "key" of the exposed entity that is being accessed.
|
|
49
|
+
*/
|
|
18
50
|
exposedEntityKey: string
|
|
51
|
+
/**
|
|
52
|
+
* The "name" of the exposed entity that is being accessed.
|
|
53
|
+
*/
|
|
54
|
+
exposedEntityName: string
|
|
55
|
+
/**
|
|
56
|
+
* The "kind" of the action that is being performed.
|
|
57
|
+
*/
|
|
19
58
|
actionKind: ActionKind
|
|
59
|
+
/**
|
|
60
|
+
* Information about each path parameter in this route.
|
|
61
|
+
*/
|
|
62
|
+
params: RouteParam[]
|
|
20
63
|
}
|
|
21
64
|
|
|
22
65
|
/**
|
|
@@ -44,6 +87,7 @@ export interface RuntimeResolvedAction {
|
|
|
44
87
|
entity: DomainEntity
|
|
45
88
|
action: Action
|
|
46
89
|
params: Record<string, string>
|
|
90
|
+
lookup: RouteLookup
|
|
47
91
|
}
|
|
48
92
|
|
|
49
93
|
export type RuleEvaluator = (rule: AccessRule) => Promise<boolean | undefined> | boolean | undefined
|
|
@@ -429,6 +473,7 @@ export class RuntimeApiModel extends ApiModel {
|
|
|
429
473
|
entity,
|
|
430
474
|
action,
|
|
431
475
|
params,
|
|
476
|
+
lookup: def.lookup,
|
|
432
477
|
}
|
|
433
478
|
}
|
|
434
479
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ActionKind, type UpdateAction } from '../actions/index.js'
|
|
2
2
|
import { ApiModel, type ApiModelSchema } from '../ApiModel.js'
|
|
3
|
-
import {
|
|
3
|
+
import { ExposedEntity } from '../ExposedEntity.js'
|
|
4
|
+
import { RouteParam, type RouteDefinition, type RuntimeApiModelSchema } from '../RuntimeApiModel.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* A class that takes the API model and generates a runtime-optimized API model.
|
|
@@ -34,27 +35,27 @@ export class RuntimeModelGenerator {
|
|
|
34
35
|
for (const action of expose.actions) {
|
|
35
36
|
switch (action.kind) {
|
|
36
37
|
case 'list':
|
|
37
|
-
if (colPath) this.addRoute('GET', colPath, expose
|
|
38
|
+
if (colPath) this.addRoute('GET', colPath, expose, action.kind)
|
|
38
39
|
break
|
|
39
40
|
case 'create':
|
|
40
|
-
if (colPath) this.addRoute('POST', colPath, expose
|
|
41
|
+
if (colPath) this.addRoute('POST', colPath, expose, action.kind)
|
|
41
42
|
break
|
|
42
43
|
case 'search':
|
|
43
|
-
if (colPath) this.addRoute('POST', `${colPath}/search`, expose
|
|
44
|
+
if (colPath) this.addRoute('POST', `${colPath}/search`, expose, action.kind)
|
|
44
45
|
break
|
|
45
46
|
case 'read':
|
|
46
|
-
if (resPath) this.addRoute('GET', resPath, expose
|
|
47
|
+
if (resPath) this.addRoute('GET', resPath, expose, action.kind)
|
|
47
48
|
break
|
|
48
49
|
case 'update':
|
|
49
50
|
if (resPath) {
|
|
50
51
|
const updateAction = action as UpdateAction
|
|
51
52
|
for (const method of updateAction.allowedMethods) {
|
|
52
|
-
this.addRoute(method, resPath, expose
|
|
53
|
+
this.addRoute(method, resPath, expose, action.kind)
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
break
|
|
56
57
|
case 'delete':
|
|
57
|
-
if (resPath) this.addRoute('DELETE', resPath, expose
|
|
58
|
+
if (resPath) this.addRoute('DELETE', resPath, expose, action.kind)
|
|
58
59
|
break
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -66,14 +67,65 @@ export class RuntimeModelGenerator {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
private addRoute(method: string, path: string,
|
|
70
|
+
private addRoute(method: string, path: string, expose: ExposedEntity, actionKind: ActionKind): void {
|
|
70
71
|
const upperMethod = method.toUpperCase()
|
|
71
72
|
if (!this.#routingMap[upperMethod]) {
|
|
72
73
|
this.#routingMap[upperMethod] = []
|
|
73
74
|
}
|
|
75
|
+
|
|
76
|
+
const domainEntity = this.#apiModel.domain?.findEntity(expose.entity.key, expose.entity.domain)
|
|
77
|
+
if (!domainEntity) throw new Error(`Entity ${expose.key} not found, this should never happen`)
|
|
78
|
+
|
|
79
|
+
const params: RouteParam[] = []
|
|
80
|
+
let current: ExposedEntity | undefined
|
|
81
|
+
let previous: ExposedEntity | undefined = undefined
|
|
82
|
+
if (['list', 'create', 'search'].includes(actionKind)) {
|
|
83
|
+
// when starting with a collection, ignore self and start with parent (if any).
|
|
84
|
+
// A collection won't have any path params anyway, so we can skip it.
|
|
85
|
+
current = expose.parent ? this.#apiModel.exposes.get(expose.parent.key) : undefined
|
|
86
|
+
// setting the previous to the current so the associations are captured.
|
|
87
|
+
previous = expose
|
|
88
|
+
} else {
|
|
89
|
+
current = expose
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
while (current) {
|
|
93
|
+
const match = current.resourcePath.match(/\{([^}]+)\}/)
|
|
94
|
+
if (match) {
|
|
95
|
+
const entity = this.#apiModel.domain?.findEntity(current.entity.key, current.entity.domain)
|
|
96
|
+
if (!entity) throw new Error(`Entity ${current.entity.key} not found, this should never happen`)
|
|
97
|
+
const pkName = entity.primaryKey()?.info.name
|
|
98
|
+
if (!pkName) throw new Error(`Entity ${current.entity.key} has no primary key, this should never happen`)
|
|
99
|
+
|
|
100
|
+
let associationName: string | undefined
|
|
101
|
+
if (previous && previous.parent && previous.parent.key === current.key) {
|
|
102
|
+
for (const assoc of entity.withInheritedAssociations()) {
|
|
103
|
+
if (previous.parent?.association.key === assoc.key) {
|
|
104
|
+
associationName = assoc.info.name
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
params.push({
|
|
111
|
+
paramName: match[1],
|
|
112
|
+
exposedEntityKey: current.key,
|
|
113
|
+
exposedEntityName: entity.info.name as string,
|
|
114
|
+
propertyName: pkName,
|
|
115
|
+
...(associationName ? { associationName } : {}),
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
previous = current
|
|
119
|
+
current = current.parent ? this.#apiModel.exposes.get(current.parent.key) : undefined
|
|
120
|
+
}
|
|
74
121
|
this.#routingMap[upperMethod].push({
|
|
75
122
|
path,
|
|
76
|
-
lookup: {
|
|
123
|
+
lookup: {
|
|
124
|
+
exposedEntityKey: expose.key,
|
|
125
|
+
exposedEntityName: domainEntity.info.name as string,
|
|
126
|
+
actionKind,
|
|
127
|
+
params,
|
|
128
|
+
},
|
|
77
129
|
})
|
|
78
130
|
}
|
|
79
131
|
}
|