@ankhorage/data-sources 1.0.1 → 2.0.0
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 +6 -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 +5 -5
- 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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
createOpenApiDiscoveryCandidates,
|
|
5
|
-
|
|
5
|
+
discoverOpenApi,
|
|
6
6
|
type ExternalApiFetch,
|
|
7
7
|
type ExternalApiFetchResponse,
|
|
8
|
-
|
|
8
|
+
introspectGraphQlApi,
|
|
9
9
|
} from './index';
|
|
10
10
|
|
|
11
11
|
function jsonResponse(value: unknown, status = 200): ExternalApiFetchResponse {
|
|
@@ -50,84 +50,85 @@ function graphQlPayload() {
|
|
|
50
50
|
} as const;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
53
|
+
it('creates OpenAPI discovery candidates and rejects unsafe URLs', () => {
|
|
54
|
+
const candidates = createOpenApiDiscoveryCandidates('https://api.example.com/service');
|
|
55
|
+
expect(candidates).toContain('https://api.example.com/service');
|
|
56
|
+
expect(candidates).toContain('https://api.example.com/service/openapi.json');
|
|
57
|
+
expect(candidates).toContain('https://api.example.com/openapi.json');
|
|
58
|
+
expect(createOpenApiDiscoveryCandidates('file:///tmp/openapi.json')).toEqual([]);
|
|
59
|
+
expect(createOpenApiDiscoveryCandidates('https://user:secret@example.com/openapi.json')).toEqual(
|
|
60
|
+
[],
|
|
61
|
+
);
|
|
62
|
+
});
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
expect(result.ok).toBe(true);
|
|
72
|
-
if (result.ok) {
|
|
73
|
-
expect(result.data).toMatchObject({ kind: 'api', origin: 'external', protocol: 'rest' });
|
|
74
|
-
expect(result.data.openApi?.url).toBe('https://api.example.com/openapi.json');
|
|
75
|
-
expect(result.attempts[0]?.outcome).toBe('matched');
|
|
76
|
-
}
|
|
64
|
+
it('discovers an external REST API', async () => {
|
|
65
|
+
const result = await discoverOpenApi({
|
|
66
|
+
id: 'inventory',
|
|
67
|
+
url: 'https://api.example.com/openapi.json',
|
|
68
|
+
fetch: () => Promise.resolve(jsonResponse(openApiDocument())),
|
|
77
69
|
});
|
|
70
|
+
expect(result.ok).toBe(true);
|
|
71
|
+
if (result.ok) {
|
|
72
|
+
expect(result.data).toMatchObject({ origin: 'external', protocol: 'rest' });
|
|
73
|
+
expect('kind' in result.data).toBe(false);
|
|
74
|
+
expect(result.data.openApi?.url).toBe('https://api.example.com/openapi.json');
|
|
75
|
+
expect(result.attempts[0]?.outcome).toBe('matched');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
expect(result.ok).toBe(true);
|
|
96
|
-
expect(calls).toEqual([
|
|
97
|
-
'https://api.example.com/service',
|
|
98
|
-
'https://api.example.com/service/openapi.json',
|
|
99
|
-
]);
|
|
100
|
-
expect(JSON.stringify(result)).not.toContain('hidden');
|
|
79
|
+
it('falls back to OpenAPI conventional locations without leaking response data', async () => {
|
|
80
|
+
const calls: string[] = [];
|
|
81
|
+
const fetch: ExternalApiFetch = (url) => {
|
|
82
|
+
calls.push(url);
|
|
83
|
+
return Promise.resolve(
|
|
84
|
+
url.endsWith('/service/openapi.json')
|
|
85
|
+
? jsonResponse(openApiDocument())
|
|
86
|
+
: jsonResponse({ private: 'hidden' }, 404),
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
const result = await discoverOpenApi({
|
|
90
|
+
id: 'inventory',
|
|
91
|
+
url: 'https://api.example.com/service',
|
|
92
|
+
conventionalPaths: ['openapi.json'],
|
|
93
|
+
fetch,
|
|
101
94
|
});
|
|
95
|
+
expect(result.ok).toBe(true);
|
|
96
|
+
expect(calls).toEqual([
|
|
97
|
+
'https://api.example.com/service',
|
|
98
|
+
'https://api.example.com/service/openapi.json',
|
|
99
|
+
]);
|
|
100
|
+
expect(JSON.stringify(result)).not.toContain('hidden');
|
|
101
|
+
});
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
110
|
-
expect(result.ok).toBe(true);
|
|
111
|
-
if (result.ok) {
|
|
112
|
-
expect(result.data).toMatchObject({ kind: 'api', origin: 'external', protocol: 'graphql' });
|
|
113
|
-
expect(result.data.endpoints.graphql?.operations['query.items']).toBeDefined();
|
|
114
|
-
expect(JSON.stringify(result)).not.toContain('server-only');
|
|
115
|
-
}
|
|
103
|
+
it('introspects GraphQL without echoing trusted headers', async () => {
|
|
104
|
+
const result = await introspectGraphQlApi({
|
|
105
|
+
id: 'catalog',
|
|
106
|
+
endpointUrl: 'https://api.example.com/graphql',
|
|
107
|
+
headers: { authorization: 'Bearer server-only' },
|
|
108
|
+
fetch: () => Promise.resolve(jsonResponse(graphQlPayload())),
|
|
116
109
|
});
|
|
110
|
+
expect(result.ok).toBe(true);
|
|
111
|
+
if (result.ok) {
|
|
112
|
+
expect(result.data).toMatchObject({ origin: 'external', protocol: 'graphql' });
|
|
113
|
+
expect('kind' in result.data).toBe(false);
|
|
114
|
+
expect(result.data.endpoints.graphql?.operations['query.items']).toBeDefined();
|
|
115
|
+
expect(JSON.stringify(result)).not.toContain('server-only');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
129
|
-
expect(http).toMatchObject({ ok: false, status: 403 });
|
|
130
|
-
expect(JSON.stringify(http)).not.toContain('hidden');
|
|
131
|
-
expect(shape.ok).toBe(false);
|
|
119
|
+
it('reports GraphQL transport and response-shape failures safely', async () => {
|
|
120
|
+
const http = await introspectGraphQlApi({
|
|
121
|
+
id: 'catalog',
|
|
122
|
+
endpointUrl: 'https://api.example.com/graphql',
|
|
123
|
+
fetch: () => Promise.resolve(jsonResponse({ secret: 'hidden' }, 403)),
|
|
124
|
+
});
|
|
125
|
+
const shape = await introspectGraphQlApi({
|
|
126
|
+
id: 'catalog',
|
|
127
|
+
endpointUrl: 'https://api.example.com/graphql',
|
|
128
|
+
fetch: () => Promise.resolve(jsonResponse({ data: {} })),
|
|
132
129
|
});
|
|
130
|
+
expect(http).toMatchObject({ ok: false, status: 403 });
|
|
131
|
+
expect(JSON.stringify(http)).not.toContain('hidden');
|
|
132
|
+
expect(shape.ok).toBe(false);
|
|
133
|
+
if (!shape.ok) expect(shape.diagnostics[0]?.apiId).toBe('catalog');
|
|
133
134
|
});
|
package/src/discovery/index.ts
CHANGED
|
@@ -2,16 +2,23 @@ import type {
|
|
|
2
2
|
CredentialRef,
|
|
3
3
|
DataContractValue,
|
|
4
4
|
DataSourceDiagnostic,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
ExternalGraphQlApiDefinition,
|
|
6
|
+
ExternalRestApiDefinition,
|
|
7
7
|
} from '@ankhorage/contracts/data';
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
createGraphQlApi,
|
|
11
11
|
createGraphQlIntrospectionRequest,
|
|
12
12
|
type GraphQlIntrospectionResult,
|
|
13
13
|
} from '../graphql';
|
|
14
14
|
import { importOpenApiDocument, type OpenApiDocumentObject } from '../openapi';
|
|
15
|
+
import {
|
|
16
|
+
isSuccessfulStatus,
|
|
17
|
+
normalizeCandidateUrl,
|
|
18
|
+
parseHttpUrl,
|
|
19
|
+
parseJsonResponse,
|
|
20
|
+
readRecord,
|
|
21
|
+
} from './http';
|
|
15
22
|
|
|
16
23
|
export const DEFAULT_OPENAPI_DISCOVERY_PATHS = [
|
|
17
24
|
'openapi.json',
|
|
@@ -50,7 +57,7 @@ export interface OpenApiDiscoveryAttempt {
|
|
|
50
57
|
readonly status?: number;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
export interface
|
|
60
|
+
export interface DiscoverOpenApiInput {
|
|
54
61
|
readonly id: string;
|
|
55
62
|
readonly url: string;
|
|
56
63
|
readonly fetch: ExternalApiFetch;
|
|
@@ -62,10 +69,10 @@ export interface DiscoverOpenApiDataSourceInput {
|
|
|
62
69
|
readonly conventionalPaths?: readonly string[];
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
export type
|
|
72
|
+
export type DiscoverOpenApiResult =
|
|
66
73
|
| {
|
|
67
74
|
readonly ok: true;
|
|
68
|
-
readonly data:
|
|
75
|
+
readonly data: ExternalRestApiDefinition;
|
|
69
76
|
readonly documentUrl: string;
|
|
70
77
|
readonly attempts: readonly OpenApiDiscoveryAttempt[];
|
|
71
78
|
readonly diagnostics: readonly DataSourceDiagnostic[];
|
|
@@ -76,7 +83,7 @@ export type DiscoverOpenApiDataSourceResult =
|
|
|
76
83
|
readonly diagnostics: readonly DataSourceDiagnostic[];
|
|
77
84
|
};
|
|
78
85
|
|
|
79
|
-
export interface
|
|
86
|
+
export interface IntrospectGraphQlApiInput {
|
|
80
87
|
readonly id: string;
|
|
81
88
|
readonly endpointUrl: string;
|
|
82
89
|
readonly fetch: ExternalApiFetch;
|
|
@@ -88,10 +95,10 @@ export interface IntrospectGraphQlDataSourceInput {
|
|
|
88
95
|
readonly metadata?: DataContractValue;
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
export type
|
|
98
|
+
export type IntrospectGraphQlApiResult =
|
|
92
99
|
| {
|
|
93
100
|
readonly ok: true;
|
|
94
|
-
readonly data:
|
|
101
|
+
readonly data: ExternalGraphQlApiDefinition;
|
|
95
102
|
readonly diagnostics: readonly DataSourceDiagnostic[];
|
|
96
103
|
}
|
|
97
104
|
| {
|
|
@@ -121,9 +128,7 @@ export function createOpenApiDiscoveryCandidates(
|
|
|
121
128
|
return [...new Set(candidates)];
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
export async function
|
|
125
|
-
input: DiscoverOpenApiDataSourceInput,
|
|
126
|
-
): Promise<DiscoverOpenApiDataSourceResult> {
|
|
131
|
+
export async function discoverOpenApi(input: DiscoverOpenApiInput): Promise<DiscoverOpenApiResult> {
|
|
127
132
|
const candidates = createOpenApiDiscoveryCandidates(input.url, input.conventionalPaths);
|
|
128
133
|
if (candidates.length === 0) {
|
|
129
134
|
return discoveryFailure(input.id, [], 'OpenAPI discovery requires a valid HTTP or HTTPS URL.');
|
|
@@ -152,29 +157,18 @@ export async function discoverOpenApiDataSource(
|
|
|
152
157
|
);
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
export async function
|
|
156
|
-
input:
|
|
157
|
-
): Promise<
|
|
160
|
+
export async function introspectGraphQlApi(
|
|
161
|
+
input: IntrospectGraphQlApiInput,
|
|
162
|
+
): Promise<IntrospectGraphQlApiResult> {
|
|
158
163
|
const endpoint = parseHttpUrl(input.endpointUrl);
|
|
159
164
|
if (endpoint === undefined) {
|
|
160
165
|
return graphqlFailure(input.id, 'GraphQL introspection requires a valid HTTP or HTTPS URL.');
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
response = await input.fetch(normalizeCandidateUrl(endpoint), {
|
|
166
|
-
method: 'POST',
|
|
167
|
-
headers: {
|
|
168
|
-
accept: 'application/json',
|
|
169
|
-
'content-type': 'application/json',
|
|
170
|
-
...(input.headers ?? {}),
|
|
171
|
-
},
|
|
172
|
-
body: JSON.stringify(createGraphQlIntrospectionRequest()),
|
|
173
|
-
});
|
|
174
|
-
} catch {
|
|
168
|
+
const response = await fetchGraphQlIntrospection(input, endpoint);
|
|
169
|
+
if (response === undefined) {
|
|
175
170
|
return graphqlFailure(input.id, 'GraphQL introspection request failed.', 'network-error');
|
|
176
171
|
}
|
|
177
|
-
|
|
178
172
|
if (!isSuccessfulStatus(response.status)) {
|
|
179
173
|
return {
|
|
180
174
|
...graphqlFailure(
|
|
@@ -195,7 +189,7 @@ export async function introspectGraphQlDataSource(
|
|
|
195
189
|
);
|
|
196
190
|
}
|
|
197
191
|
|
|
198
|
-
const result =
|
|
192
|
+
const result = createGraphQlApi({
|
|
199
193
|
id: input.id,
|
|
200
194
|
endpointUrl: normalizeCandidateUrl(endpoint),
|
|
201
195
|
credential: input.credential,
|
|
@@ -206,19 +200,37 @@ export async function introspectGraphQlDataSource(
|
|
|
206
200
|
schemaVersion: input.schemaVersion,
|
|
207
201
|
metadata: input.metadata,
|
|
208
202
|
});
|
|
209
|
-
|
|
210
203
|
return result.ok
|
|
211
204
|
? { ok: true, data: result.data, diagnostics: result.diagnostics ?? [] }
|
|
212
205
|
: { ok: false, diagnostics: result.diagnostics };
|
|
213
206
|
}
|
|
214
207
|
|
|
208
|
+
async function fetchGraphQlIntrospection(
|
|
209
|
+
input: IntrospectGraphQlApiInput,
|
|
210
|
+
endpoint: URL,
|
|
211
|
+
): Promise<ExternalApiFetchResponse | undefined> {
|
|
212
|
+
try {
|
|
213
|
+
return await input.fetch(normalizeCandidateUrl(endpoint), {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: {
|
|
216
|
+
accept: 'application/json',
|
|
217
|
+
'content-type': 'application/json',
|
|
218
|
+
...(input.headers ?? {}),
|
|
219
|
+
},
|
|
220
|
+
body: JSON.stringify(createGraphQlIntrospectionRequest()),
|
|
221
|
+
});
|
|
222
|
+
} catch {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
215
227
|
interface OpenApiProbeResult {
|
|
216
228
|
readonly attempt: OpenApiDiscoveryAttempt;
|
|
217
229
|
readonly result?: ReturnType<typeof importOpenApiDocument>;
|
|
218
230
|
}
|
|
219
231
|
|
|
220
232
|
async function probeOpenApiCandidate(
|
|
221
|
-
input:
|
|
233
|
+
input: DiscoverOpenApiInput,
|
|
222
234
|
candidate: string,
|
|
223
235
|
): Promise<OpenApiProbeResult> {
|
|
224
236
|
let response: ExternalApiFetchResponse;
|
|
@@ -265,35 +277,6 @@ async function probeOpenApiCandidate(
|
|
|
265
277
|
};
|
|
266
278
|
}
|
|
267
279
|
|
|
268
|
-
function parseHttpUrl(rawUrl: string): URL | undefined {
|
|
269
|
-
try {
|
|
270
|
-
const parsed = new URL(rawUrl.trim());
|
|
271
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return undefined;
|
|
272
|
-
if (parsed.username.length > 0 || parsed.password.length > 0) return undefined;
|
|
273
|
-
return parsed;
|
|
274
|
-
} catch {
|
|
275
|
-
return undefined;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function normalizeCandidateUrl(url: URL): string {
|
|
280
|
-
const normalized = new URL(url.toString());
|
|
281
|
-
normalized.hash = '';
|
|
282
|
-
return normalized.toString();
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function isSuccessfulStatus(status: number): boolean {
|
|
286
|
-
return status >= 200 && status < 300;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function parseJsonResponse(response: ExternalApiFetchResponse): Promise<unknown> {
|
|
290
|
-
try {
|
|
291
|
-
return JSON.parse(await response.text()) as unknown;
|
|
292
|
-
} catch {
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
280
|
function isOpenApiDocument(value: unknown): value is OpenApiDocumentObject {
|
|
298
281
|
const record = readRecord(value);
|
|
299
282
|
return (
|
|
@@ -310,31 +293,25 @@ function readGraphQlIntrospection(value: unknown): GraphQlIntrospectionResult |
|
|
|
310
293
|
return data;
|
|
311
294
|
}
|
|
312
295
|
|
|
313
|
-
function readRecord(value: unknown): Readonly<Record<string, unknown>> | undefined {
|
|
314
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
315
|
-
? (value as Readonly<Record<string, unknown>>)
|
|
316
|
-
: undefined;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
296
|
function discoveryFailure(
|
|
320
|
-
|
|
297
|
+
apiId: string,
|
|
321
298
|
attempts: readonly OpenApiDiscoveryAttempt[],
|
|
322
299
|
message: string,
|
|
323
|
-
):
|
|
300
|
+
): DiscoverOpenApiResult {
|
|
324
301
|
return {
|
|
325
302
|
ok: false,
|
|
326
303
|
attempts,
|
|
327
|
-
diagnostics: [{ code: 'missing-schema',
|
|
304
|
+
diagnostics: [{ code: 'missing-schema', apiId, message, severity: 'error' }],
|
|
328
305
|
};
|
|
329
306
|
}
|
|
330
307
|
|
|
331
308
|
function graphqlFailure(
|
|
332
|
-
|
|
309
|
+
apiId: string,
|
|
333
310
|
message: string,
|
|
334
311
|
code: DataSourceDiagnostic['code'] = 'invalid-config',
|
|
335
|
-
): Extract<
|
|
312
|
+
): Extract<IntrospectGraphQlApiResult, { readonly ok: false }> {
|
|
336
313
|
return {
|
|
337
314
|
ok: false,
|
|
338
|
-
diagnostics: [{ code,
|
|
315
|
+
diagnostics: [{ code, apiId, message, severity: 'error' }],
|
|
339
316
|
};
|
|
340
317
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
createGraphQlApi,
|
|
5
5
|
createGraphQlIntrospectionRequest,
|
|
6
6
|
type GraphQlIntrospectionResult,
|
|
7
7
|
normalizeGraphQlIntrospectionOperations,
|
|
@@ -46,7 +46,7 @@ function introspection(): GraphQlIntrospectionResult {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
describe('GraphQL normalization', () => {
|
|
49
|
+
describe('GraphQL API normalization', () => {
|
|
50
50
|
it('creates the standard introspection request and stable IDs', () => {
|
|
51
51
|
expect(createGraphQlIntrospectionRequest().query).toContain('__schema');
|
|
52
52
|
expect(normalizeGraphQlOperationId('query', 'Posts')).toBe('query.posts');
|
|
@@ -59,25 +59,23 @@ describe('GraphQL normalization', () => {
|
|
|
59
59
|
);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it('creates
|
|
63
|
-
const result =
|
|
62
|
+
it('creates a canonical external GraphQL API', () => {
|
|
63
|
+
const result = createGraphQlApi({
|
|
64
64
|
id: 'content',
|
|
65
65
|
endpointUrl: 'https://example.com/graphql',
|
|
66
66
|
introspection: introspection(),
|
|
67
67
|
});
|
|
68
68
|
expect(result.ok).toBe(true);
|
|
69
69
|
if (result.ok) {
|
|
70
|
-
expect(result.data).toMatchObject({
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
protocol: 'graphql',
|
|
74
|
-
});
|
|
70
|
+
expect(result.data).toMatchObject({ origin: 'external', protocol: 'graphql' });
|
|
71
|
+
expect('kind' in result.data).toBe(false);
|
|
72
|
+
expect(result.data.endpoints.graphql?.baseUrl).toBeUndefined();
|
|
75
73
|
expect(result.data.endpoints.graphql?.operations['query.posts']?.intent).toBe('read');
|
|
76
74
|
}
|
|
77
75
|
});
|
|
78
76
|
|
|
79
77
|
it('supports manual operations and validates endpoint URLs', () => {
|
|
80
|
-
const manual =
|
|
78
|
+
const manual = createGraphQlApi({
|
|
81
79
|
id: 'manual',
|
|
82
80
|
endpointUrl: 'https://example.com/graphql',
|
|
83
81
|
introspectionEnabled: false,
|
|
@@ -89,8 +87,9 @@ describe('GraphQL normalization', () => {
|
|
|
89
87
|
},
|
|
90
88
|
],
|
|
91
89
|
});
|
|
92
|
-
const invalid =
|
|
90
|
+
const invalid = createGraphQlApi({ id: 'broken', endpointUrl: ' ' });
|
|
93
91
|
expect(manual.ok).toBe(true);
|
|
94
92
|
expect(invalid.ok).toBe(false);
|
|
93
|
+
if (!invalid.ok) expect(invalid.diagnostics[0]?.apiId).toBe('broken');
|
|
95
94
|
});
|
|
96
95
|
});
|