@ankhorage/data-sources 1.0.1 → 2.0.1
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/CHANGELOG.md +12 -0
- package/README.md +6 -41
- package/dist/ankh.provider.js +1 -1
- package/dist/ankh.provider.js.map +1 -1
- package/dist/discovery/http.d.ts +10 -0
- package/dist/discovery/http.d.ts.map +1 -0
- package/dist/discovery/http.js +35 -0
- package/dist/discovery/http.js.map +1 -0
- package/dist/discovery/index.d.ts +9 -9
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +27 -55
- package/dist/discovery/index.js.map +1 -1
- package/dist/graphql/index.d.ts +8 -76
- package/dist/graphql/index.d.ts.map +1 -1
- package/dist/graphql/index.js +17 -197
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/introspection.d.ts +5 -0
- package/dist/graphql/introspection.d.ts.map +1 -0
- package/dist/graphql/introspection.js +136 -0
- package/dist/graphql/introspection.js.map +1 -0
- package/dist/graphql/operation.d.ts +5 -0
- package/dist/graphql/operation.d.ts.map +1 -0
- package/dist/graphql/operation.js +39 -0
- package/dist/graphql/operation.js.map +1 -0
- package/dist/graphql/types.d.ts +71 -0
- package/dist/graphql/types.d.ts.map +1 -0
- package/dist/graphql/types.js +2 -0
- package/dist/graphql/types.js.map +1 -0
- package/dist/index.d.ts +1 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -34
- package/dist/index.js.map +1 -1
- package/dist/openapi/endpoints.d.ts +5 -0
- package/dist/openapi/endpoints.d.ts.map +1 -0
- package/dist/openapi/endpoints.js +77 -0
- package/dist/openapi/endpoints.js.map +1 -0
- package/dist/openapi/index.d.ts +7 -87
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +8 -267
- package/dist/openapi/index.js.map +1 -1
- package/dist/openapi/operation.d.ts +5 -0
- package/dist/openapi/operation.d.ts.map +1 -0
- package/dist/openapi/operation.js +141 -0
- package/dist/openapi/operation.js.map +1 -0
- package/dist/openapi/schema.d.ts +5 -0
- package/dist/openapi/schema.d.ts.map +1 -0
- package/dist/openapi/schema.js +84 -0
- package/dist/openapi/schema.js.map +1 -0
- package/dist/openapi/types.d.ts +82 -0
- package/dist/openapi/types.d.ts.map +1 -0
- package/dist/openapi/types.js +2 -0
- package/dist/openapi/types.js.map +1 -0
- package/dist/packageInfo.d.ts +18 -0
- package/dist/packageInfo.d.ts.map +1 -0
- package/dist/packageInfo.js +30 -0
- package/dist/packageInfo.js.map +1 -0
- package/dist/rest/index.d.ts +5 -5
- package/dist/rest/index.d.ts.map +1 -1
- package/dist/rest/index.js +49 -48
- package/dist/rest/index.js.map +1 -1
- package/dist/test-runner/index.d.ts +2 -57
- package/dist/test-runner/index.d.ts.map +1 -1
- package/dist/test-runner/index.js +58 -160
- package/dist/test-runner/index.js.map +1 -1
- package/dist/test-runner/response.d.ts +3 -0
- package/dist/test-runner/response.d.ts.map +1 -0
- package/dist/test-runner/response.js +29 -0
- package/dist/test-runner/response.js.map +1 -0
- package/dist/test-runner/result.d.ts +6 -0
- package/dist/test-runner/result.d.ts.map +1 -0
- package/dist/test-runner/result.js +17 -0
- package/dist/test-runner/result.js.map +1 -0
- package/dist/test-runner/serialization.d.ts +12 -0
- package/dist/test-runner/serialization.d.ts.map +1 -0
- package/dist/test-runner/serialization.js +99 -0
- package/dist/test-runner/serialization.js.map +1 -0
- package/dist/test-runner/types.d.ts +58 -0
- package/dist/test-runner/types.d.ts.map +1 -0
- package/dist/test-runner/types.js +2 -0
- package/dist/test-runner/types.js.map +1 -0
- package/package.json +12 -13
- package/src/ankh.provider.ts +1 -1
- package/src/api-phase1.contract.test.ts +24 -0
- package/src/discovery/http.ts +38 -0
- package/src/discovery/index.test.ts +76 -75
- package/src/discovery/index.ts +49 -72
- package/src/graphql/index.test.ts +10 -11
- package/src/graphql/index.ts +35 -315
- package/src/graphql/introspection.ts +163 -0
- package/src/graphql/operation.ts +60 -0
- package/src/graphql/types.ts +79 -0
- package/src/index.test.ts +21 -13
- package/src/index.ts +1 -54
- package/src/openapi/endpoints.ts +124 -0
- package/src/openapi/index.test.ts +47 -47
- package/src/openapi/index.ts +12 -467
- package/src/openapi/operation.ts +231 -0
- package/src/openapi/schema.ts +127 -0
- package/src/openapi/types.ts +93 -0
- package/src/packageInfo.ts +50 -0
- package/src/rest/index.test.ts +17 -14
- package/src/rest/index.ts +67 -61
- package/src/test-runner/index.test.ts +153 -120
- package/src/test-runner/index.ts +97 -299
- package/src/test-runner/response.ts +32 -0
- package/src/test-runner/result.ts +35 -0
- package/src/test-runner/serialization.ts +151 -0
- package/src/test-runner/types.ts +81 -0
- package/dist/generated-api/index.d.ts +0 -10
- package/dist/generated-api/index.d.ts.map +0 -1
- package/dist/generated-api/index.js +0 -274
- package/dist/generated-api/index.js.map +0 -1
- package/dist/generated-api/validateGeneratedApiCollection.d.ts +0 -3
- package/dist/generated-api/validateGeneratedApiCollection.d.ts.map +0 -1
- package/dist/generated-api/validateGeneratedApiCollection.js +0 -35
- package/dist/generated-api/validateGeneratedApiCollection.js.map +0 -1
- package/src/generated-api/index.test.ts +0 -173
- package/src/generated-api/index.ts +0 -414
- package/src/generated-api/validateGeneratedApiCollection.test.ts +0 -91
- package/src/generated-api/validateGeneratedApiCollection.ts +0 -76
package/src/rest/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
DataSourceDiagnostic,
|
|
14
14
|
DataSourceDiagnosticResult,
|
|
15
15
|
EndpointId,
|
|
16
|
-
|
|
16
|
+
ExternalRestApiDefinition,
|
|
17
17
|
OperationId,
|
|
18
18
|
} from '@ankhorage/contracts/data';
|
|
19
19
|
|
|
@@ -46,7 +46,7 @@ export interface ManualRestEndpointDefinition {
|
|
|
46
46
|
readonly metadata?: DataContractValue;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export interface
|
|
49
|
+
export interface ManualRestApiDefinition {
|
|
50
50
|
readonly id: string;
|
|
51
51
|
readonly baseUrl: string;
|
|
52
52
|
readonly name?: string;
|
|
@@ -83,16 +83,16 @@ export function extractRestPathParams(path: string): readonly string[] {
|
|
|
83
83
|
return [...params];
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function
|
|
87
|
-
definition:
|
|
86
|
+
export function validateManualRestApi(
|
|
87
|
+
definition: ManualRestApiDefinition,
|
|
88
88
|
): readonly DataSourceDiagnostic[] {
|
|
89
89
|
const diagnostics: DataSourceDiagnostic[] = [];
|
|
90
90
|
|
|
91
91
|
if (definition.baseUrl.trim().length === 0) {
|
|
92
92
|
diagnostics.push({
|
|
93
93
|
code: 'invalid-config',
|
|
94
|
-
|
|
95
|
-
message: 'Manual REST
|
|
94
|
+
apiId: definition.id,
|
|
95
|
+
message: 'Manual REST API requires a non-empty baseUrl.',
|
|
96
96
|
path: 'baseUrl',
|
|
97
97
|
severity: 'error',
|
|
98
98
|
});
|
|
@@ -102,7 +102,7 @@ export function validateManualRestDataSource(
|
|
|
102
102
|
if (!endpoint.path.startsWith('/')) {
|
|
103
103
|
diagnostics.push({
|
|
104
104
|
code: 'invalid-config',
|
|
105
|
-
|
|
105
|
+
apiId: definition.id,
|
|
106
106
|
endpointId: endpoint.id,
|
|
107
107
|
message: 'Manual REST endpoint paths must start with `/`.',
|
|
108
108
|
path: `endpoints.${endpoint.id}.path`,
|
|
@@ -118,69 +118,30 @@ export function validateManualRestDataSource(
|
|
|
118
118
|
return diagnostics;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
export function
|
|
122
|
-
definition:
|
|
123
|
-
): DataSourceDiagnosticResult<
|
|
124
|
-
const diagnostics =
|
|
121
|
+
export function createManualRestApi(
|
|
122
|
+
definition: ManualRestApiDefinition,
|
|
123
|
+
): DataSourceDiagnosticResult<ExternalRestApiDefinition> {
|
|
124
|
+
const diagnostics = validateManualRestApi(definition);
|
|
125
125
|
if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
|
126
126
|
return { ok: false, diagnostics };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
return {
|
|
130
130
|
ok: true,
|
|
131
|
-
data:
|
|
131
|
+
data: normalizeManualRestApi(definition),
|
|
132
132
|
diagnostics,
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export function
|
|
137
|
-
definition:
|
|
138
|
-
):
|
|
139
|
-
const endpoints
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const operations: Record<OperationId, DataOperationConfig> = {};
|
|
143
|
-
|
|
144
|
-
for (const operation of endpoint.operations) {
|
|
145
|
-
const { parameters } = operation;
|
|
146
|
-
const request: DataOperationRequest | undefined =
|
|
147
|
-
operation.request === undefined && parameters === undefined
|
|
148
|
-
? undefined
|
|
149
|
-
: { ...operation.request, parameters };
|
|
150
|
-
|
|
151
|
-
operations[operation.id] = {
|
|
152
|
-
id: operation.id,
|
|
153
|
-
endpointId: endpoint.id,
|
|
154
|
-
name: operation.name,
|
|
155
|
-
description: operation.description,
|
|
156
|
-
protocol: 'http',
|
|
157
|
-
intent: operation.intent,
|
|
158
|
-
method: normalizeManualRestMethod(operation.method),
|
|
159
|
-
path: operation.path ?? endpoint.path,
|
|
160
|
-
request,
|
|
161
|
-
response: operation.response,
|
|
162
|
-
pagination: operation.pagination,
|
|
163
|
-
credential: operation.credential,
|
|
164
|
-
metadata: operation.metadata,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
endpoints[endpoint.id] = {
|
|
169
|
-
id: endpoint.id,
|
|
170
|
-
kind: 'http',
|
|
171
|
-
name: endpoint.name,
|
|
172
|
-
description: endpoint.description,
|
|
173
|
-
baseUrl: definition.baseUrl,
|
|
174
|
-
path: endpoint.path,
|
|
175
|
-
credential: endpoint.credential,
|
|
176
|
-
operations,
|
|
177
|
-
metadata: endpoint.metadata,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
136
|
+
export function normalizeManualRestApi(
|
|
137
|
+
definition: ManualRestApiDefinition,
|
|
138
|
+
): ExternalRestApiDefinition {
|
|
139
|
+
const endpoints = Object.fromEntries(
|
|
140
|
+
definition.endpoints.map((endpoint) => [endpoint.id, normalizeManualRestEndpoint(endpoint)]),
|
|
141
|
+
);
|
|
180
142
|
|
|
181
143
|
return {
|
|
182
144
|
id: definition.id,
|
|
183
|
-
kind: 'api',
|
|
184
145
|
origin: 'external',
|
|
185
146
|
protocol: 'rest',
|
|
186
147
|
name: definition.name,
|
|
@@ -193,8 +154,53 @@ export function normalizeManualRestDataSource(
|
|
|
193
154
|
};
|
|
194
155
|
}
|
|
195
156
|
|
|
157
|
+
function normalizeManualRestEndpoint(endpoint: ManualRestEndpointDefinition): DataEndpointConfig {
|
|
158
|
+
const operations = Object.fromEntries(
|
|
159
|
+
endpoint.operations.map((operation) => [
|
|
160
|
+
operation.id,
|
|
161
|
+
normalizeManualRestOperation(endpoint, operation),
|
|
162
|
+
]),
|
|
163
|
+
);
|
|
164
|
+
return {
|
|
165
|
+
id: endpoint.id,
|
|
166
|
+
kind: 'http',
|
|
167
|
+
name: endpoint.name,
|
|
168
|
+
description: endpoint.description,
|
|
169
|
+
path: endpoint.path,
|
|
170
|
+
credential: endpoint.credential,
|
|
171
|
+
operations,
|
|
172
|
+
metadata: endpoint.metadata,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeManualRestOperation(
|
|
177
|
+
endpoint: ManualRestEndpointDefinition,
|
|
178
|
+
operation: ManualRestOperationDefinition,
|
|
179
|
+
): DataOperationConfig {
|
|
180
|
+
const { parameters } = operation;
|
|
181
|
+
const request: DataOperationRequest | undefined =
|
|
182
|
+
operation.request === undefined && parameters === undefined
|
|
183
|
+
? undefined
|
|
184
|
+
: { ...operation.request, parameters };
|
|
185
|
+
return {
|
|
186
|
+
id: operation.id,
|
|
187
|
+
endpointId: endpoint.id,
|
|
188
|
+
name: operation.name,
|
|
189
|
+
description: operation.description,
|
|
190
|
+
protocol: 'http',
|
|
191
|
+
intent: operation.intent,
|
|
192
|
+
method: normalizeManualRestMethod(operation.method),
|
|
193
|
+
path: operation.path ?? endpoint.path,
|
|
194
|
+
request,
|
|
195
|
+
response: operation.response,
|
|
196
|
+
pagination: operation.pagination,
|
|
197
|
+
credential: operation.credential,
|
|
198
|
+
metadata: operation.metadata,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
196
202
|
function validateManualRestOperation(
|
|
197
|
-
definition:
|
|
203
|
+
definition: ManualRestApiDefinition,
|
|
198
204
|
endpoint: ManualRestEndpointDefinition,
|
|
199
205
|
operation: ManualRestOperationDefinition,
|
|
200
206
|
diagnostics: DataSourceDiagnostic[],
|
|
@@ -209,7 +215,7 @@ function validateManualRestOperation(
|
|
|
209
215
|
if (!isManualRestMethod(method)) {
|
|
210
216
|
diagnostics.push({
|
|
211
217
|
code: 'invalid-config',
|
|
212
|
-
|
|
218
|
+
apiId: definition.id,
|
|
213
219
|
endpointId: endpoint.id,
|
|
214
220
|
operationId: operation.id,
|
|
215
221
|
message: `Manual REST operation method must be one of ${REST_METHODS.join(', ')}.`,
|
|
@@ -222,7 +228,7 @@ function validateManualRestOperation(
|
|
|
222
228
|
if (pathParameterNames.has(templateParam)) continue;
|
|
223
229
|
diagnostics.push({
|
|
224
230
|
code: 'invalid-config',
|
|
225
|
-
|
|
231
|
+
apiId: definition.id,
|
|
226
232
|
endpointId: endpoint.id,
|
|
227
233
|
operationId: operation.id,
|
|
228
234
|
message: `Path template parameter '${templateParam}' must have a matching path parameter definition.`,
|
|
@@ -235,7 +241,7 @@ function validateManualRestOperation(
|
|
|
235
241
|
if (templateParams.includes(pathParameter.name)) continue;
|
|
236
242
|
diagnostics.push({
|
|
237
243
|
code: 'invalid-config',
|
|
238
|
-
|
|
244
|
+
apiId: definition.id,
|
|
239
245
|
endpointId: endpoint.id,
|
|
240
246
|
operationId: operation.id,
|
|
241
247
|
message: `Path parameter '${pathParameter.name}' is not referenced by the operation path template.`,
|
|
@@ -1,12 +1,37 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { ApiDefinition, ExternalRestApiDefinition } from '@ankhorage/contracts/data';
|
|
2
|
+
import { expect, it } from 'bun:test';
|
|
3
3
|
|
|
4
4
|
import { buildEndpointTestRequest, type EndpointTestFetch, testEndpoint } from './index';
|
|
5
5
|
|
|
6
|
-
function
|
|
6
|
+
function nutritionApi(): ExternalRestApiDefinition {
|
|
7
|
+
return {
|
|
8
|
+
id: 'nutrition',
|
|
9
|
+
origin: 'external',
|
|
10
|
+
protocol: 'rest',
|
|
11
|
+
baseUrl: 'https://api.ankhorage.com/v1/nutrition',
|
|
12
|
+
endpoints: {
|
|
13
|
+
products: {
|
|
14
|
+
id: 'products',
|
|
15
|
+
kind: 'http',
|
|
16
|
+
path: '/products',
|
|
17
|
+
operations: {
|
|
18
|
+
'products.list': {
|
|
19
|
+
id: 'products.list',
|
|
20
|
+
endpointId: 'products',
|
|
21
|
+
protocol: 'http',
|
|
22
|
+
intent: 'read',
|
|
23
|
+
method: 'GET',
|
|
24
|
+
path: '/products',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parameterApi(): ExternalRestApiDefinition {
|
|
7
33
|
return {
|
|
8
34
|
id: 'cms',
|
|
9
|
-
kind: 'api',
|
|
10
35
|
origin: 'external',
|
|
11
36
|
protocol: 'rest',
|
|
12
37
|
baseUrl: 'https://cms.example.com',
|
|
@@ -15,7 +40,6 @@ function restSource(): DataSourceConfig {
|
|
|
15
40
|
posts: {
|
|
16
41
|
id: 'posts',
|
|
17
42
|
kind: 'http',
|
|
18
|
-
path: '/posts/{postId}',
|
|
19
43
|
operations: {
|
|
20
44
|
'posts.get': {
|
|
21
45
|
id: 'posts.get',
|
|
@@ -26,13 +50,9 @@ function restSource(): DataSourceConfig {
|
|
|
26
50
|
path: '/posts/{postId}',
|
|
27
51
|
request: {
|
|
28
52
|
parameters: [
|
|
29
|
-
{
|
|
30
|
-
name: 'postId',
|
|
31
|
-
location: 'path',
|
|
32
|
-
required: true,
|
|
33
|
-
schema: { type: 'string' },
|
|
34
|
-
},
|
|
53
|
+
{ name: 'postId', location: 'path', required: true, schema: { type: 'string' } },
|
|
35
54
|
{ name: 'preview', location: 'query', schema: { type: 'boolean' } },
|
|
55
|
+
{ name: 'x-locale', location: 'header', schema: { type: 'string' } },
|
|
36
56
|
],
|
|
37
57
|
},
|
|
38
58
|
},
|
|
@@ -43,6 +63,13 @@ function restSource(): DataSourceConfig {
|
|
|
43
63
|
intent: 'create',
|
|
44
64
|
method: 'POST',
|
|
45
65
|
path: '/posts',
|
|
66
|
+
request: {
|
|
67
|
+
contentType: 'application/json',
|
|
68
|
+
parameters: [
|
|
69
|
+
{ name: 'title', location: 'body', required: true, schema: { type: 'string' } },
|
|
70
|
+
{ name: 'published', location: 'body', schema: { type: 'boolean' } },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
46
73
|
},
|
|
47
74
|
},
|
|
48
75
|
},
|
|
@@ -50,10 +77,9 @@ function restSource(): DataSourceConfig {
|
|
|
50
77
|
};
|
|
51
78
|
}
|
|
52
79
|
|
|
53
|
-
function
|
|
80
|
+
function graphQlApi(): ApiDefinition {
|
|
54
81
|
return {
|
|
55
82
|
id: 'content',
|
|
56
|
-
kind: 'api',
|
|
57
83
|
origin: 'external',
|
|
58
84
|
protocol: 'graphql',
|
|
59
85
|
endpointUrl: 'https://content.example.com/graphql',
|
|
@@ -75,124 +101,131 @@ function graphQlSource(): DataSourceConfig {
|
|
|
75
101
|
};
|
|
76
102
|
}
|
|
77
103
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
dryRun: true,
|
|
85
|
-
values: { postId: 'post 1', preview: true },
|
|
86
|
-
credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
|
|
87
|
-
});
|
|
88
|
-
expect(result.ok).toBe(true);
|
|
89
|
-
if (result.ok) {
|
|
90
|
-
expect(result.request.url).toBe('https://cms.example.com/posts/post%201?preview=true');
|
|
91
|
-
expect(result.request.headers.authorization).toBe('Bearer token');
|
|
92
|
-
}
|
|
104
|
+
it('builds the canonical Nutrition products URL', async () => {
|
|
105
|
+
const result = await buildEndpointTestRequest({
|
|
106
|
+
api: nutritionApi(),
|
|
107
|
+
endpointId: 'products',
|
|
108
|
+
operationId: 'products.list',
|
|
109
|
+
dryRun: true,
|
|
93
110
|
});
|
|
111
|
+
expect(result.ok).toBe(true);
|
|
112
|
+
if (result.ok) {
|
|
113
|
+
expect(result.request.apiId).toBe('nutrition');
|
|
114
|
+
expect(result.request.method).toBe('GET');
|
|
115
|
+
expect(result.request.url).toBe('https://api.ankhorage.com/v1/nutrition/products');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
it('serializes path, query and header parameters with credentials', async () => {
|
|
120
|
+
const result = await buildEndpointTestRequest({
|
|
121
|
+
api: parameterApi(),
|
|
122
|
+
endpointId: 'posts',
|
|
123
|
+
operationId: 'posts.get',
|
|
124
|
+
dryRun: true,
|
|
125
|
+
values: { postId: 'post 1', preview: true, 'x-locale': 'de-CH' },
|
|
126
|
+
credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
|
|
127
|
+
});
|
|
128
|
+
expect(result.ok).toBe(true);
|
|
129
|
+
if (result.ok) {
|
|
130
|
+
expect(result.request.url).toBe('https://cms.example.com/posts/post%201?preview=true');
|
|
131
|
+
expect(result.request.headers).toEqual({
|
|
132
|
+
authorization: 'Bearer token',
|
|
133
|
+
'x-locale': 'de-CH',
|
|
105
134
|
});
|
|
106
|
-
|
|
107
|
-
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('serializes body parameters without provider-specific handling', async () => {
|
|
139
|
+
const result = await buildEndpointTestRequest({
|
|
140
|
+
api: parameterApi(),
|
|
141
|
+
endpointId: 'posts',
|
|
142
|
+
operationId: 'posts.create',
|
|
143
|
+
dryRun: true,
|
|
144
|
+
values: { title: 'Hello', published: true },
|
|
145
|
+
credentialResolver: () => ({ headers: {} }),
|
|
108
146
|
});
|
|
147
|
+
expect(result.ok).toBe(true);
|
|
148
|
+
if (result.ok) {
|
|
149
|
+
expect(result.request.body).toBe('{"title":"Hello","published":true}');
|
|
150
|
+
expect(result.request.headers['content-type']).toBe('application/json');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
109
153
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
});
|
|
119
|
-
const network = await testEndpoint({
|
|
120
|
-
dataSource: restSource(),
|
|
121
|
-
endpointId: 'posts',
|
|
122
|
-
operationId: 'posts.get',
|
|
123
|
-
values: { postId: 'x' },
|
|
124
|
-
credentialResolver: () => ({ headers: {} }),
|
|
125
|
-
fetch: () => Promise.reject(new Error('Connection failed')),
|
|
126
|
-
});
|
|
127
|
-
const credential = await buildEndpointTestRequest({
|
|
128
|
-
dataSource: restSource(),
|
|
129
|
-
endpointId: 'posts',
|
|
130
|
-
operationId: 'posts.get',
|
|
131
|
-
values: { postId: 'x' },
|
|
132
|
-
});
|
|
133
|
-
const input = await buildEndpointTestRequest({
|
|
134
|
-
dataSource: restSource(),
|
|
135
|
-
endpointId: 'posts',
|
|
136
|
-
operationId: 'posts.get',
|
|
137
|
-
credentialResolver: () => ({ headers: {} }),
|
|
138
|
-
});
|
|
139
|
-
expect(http.ok).toBe(false);
|
|
140
|
-
expect(network.ok).toBe(false);
|
|
141
|
-
expect(credential.ok).toBe(false);
|
|
142
|
-
expect(input.ok).toBe(false);
|
|
154
|
+
it('executes external REST requests and parses responses', async () => {
|
|
155
|
+
const fetch: EndpointTestFetch = () =>
|
|
156
|
+
Promise.resolve({ status: 200, text: () => Promise.resolve('{"products":[]}') });
|
|
157
|
+
const result = await testEndpoint({
|
|
158
|
+
api: nutritionApi(),
|
|
159
|
+
endpointId: 'products',
|
|
160
|
+
operationId: 'products.list',
|
|
161
|
+
fetch,
|
|
143
162
|
});
|
|
163
|
+
expect(result.ok).toBe(true);
|
|
164
|
+
if (result.ok) expect(result.data).toEqual({ products: [] });
|
|
165
|
+
});
|
|
144
166
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
credentialResolver: () => ({ headers: {} }),
|
|
153
|
-
});
|
|
154
|
-
const graphql = await buildEndpointTestRequest({
|
|
155
|
-
dataSource: graphQlSource(),
|
|
156
|
-
endpointId: 'graphql',
|
|
157
|
-
operationId: 'query.viewer',
|
|
158
|
-
dryRun: true,
|
|
159
|
-
values: { variables: { id: 'viewer-1' } },
|
|
160
|
-
});
|
|
161
|
-
expect(post.ok).toBe(true);
|
|
162
|
-
if (post.ok) expect(post.request.body).toBe('{"title":"Hello"}');
|
|
163
|
-
expect(graphql.ok).toBe(true);
|
|
164
|
-
if (graphql.ok) expect(graphql.request.url).toBe('https://content.example.com/graphql');
|
|
167
|
+
it('builds GraphQL requests through the same canonical API input', async () => {
|
|
168
|
+
const result = await buildEndpointTestRequest({
|
|
169
|
+
api: graphQlApi(),
|
|
170
|
+
endpointId: 'graphql',
|
|
171
|
+
operationId: 'query.viewer',
|
|
172
|
+
dryRun: true,
|
|
173
|
+
values: { variables: { id: 'viewer-1' } },
|
|
165
174
|
});
|
|
175
|
+
expect(result.ok).toBe(true);
|
|
176
|
+
if (result.ok) {
|
|
177
|
+
expect(result.request.url).toBe('https://content.example.com/graphql');
|
|
178
|
+
expect(result.request.method).toBe('POST');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
166
181
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
it('rejects internal APIs during phase 1', async () => {
|
|
183
|
+
const internal: ApiDefinition = {
|
|
184
|
+
id: 'future-api',
|
|
185
|
+
origin: 'internal',
|
|
186
|
+
protocol: 'rest',
|
|
187
|
+
basePath: '/v1/future',
|
|
188
|
+
endpoints: {},
|
|
189
|
+
};
|
|
190
|
+
const result = await buildEndpointTestRequest({
|
|
191
|
+
api: internal,
|
|
192
|
+
endpointId: 'products',
|
|
193
|
+
operationId: 'products.list',
|
|
194
|
+
});
|
|
195
|
+
expect(result.ok).toBe(false);
|
|
196
|
+
if (!result.ok) {
|
|
197
|
+
expect(result.diagnostics[0]?.apiId).toBe('future-api');
|
|
198
|
+
expect(result.diagnostics[0]?.message).toContain('not executable in API phase 1');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('never treats database protocol operations as API HTTP operations', async () => {
|
|
203
|
+
const api: ExternalRestApiDefinition = {
|
|
204
|
+
id: 'broken-api',
|
|
205
|
+
origin: 'external',
|
|
206
|
+
protocol: 'rest',
|
|
207
|
+
baseUrl: 'https://example.com',
|
|
208
|
+
endpoints: {
|
|
209
|
+
products: {
|
|
210
|
+
id: 'products',
|
|
211
|
+
kind: 'http',
|
|
212
|
+
operations: {
|
|
213
|
+
'products.list': {
|
|
214
|
+
id: 'products.list',
|
|
215
|
+
endpointId: 'products',
|
|
216
|
+
protocol: 'database',
|
|
217
|
+
intent: 'read',
|
|
218
|
+
path: '/products',
|
|
186
219
|
},
|
|
187
220
|
},
|
|
188
221
|
},
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expect(result.ok).toBe(false);
|
|
196
|
-
if (!result.ok) expect(result.diagnostics[0]?.message).toContain('adapter executor');
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
const result = await buildEndpointTestRequest({
|
|
225
|
+
api,
|
|
226
|
+
endpointId: 'products',
|
|
227
|
+
operationId: 'products.list',
|
|
197
228
|
});
|
|
229
|
+
expect(result.ok).toBe(false);
|
|
230
|
+
if (!result.ok) expect(result.diagnostics[0]?.message).toContain("protocol 'database'");
|
|
198
231
|
});
|