@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.
Files changed (120) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +6 -41
  3. package/dist/ankh.provider.js +1 -1
  4. package/dist/ankh.provider.js.map +1 -1
  5. package/dist/discovery/http.d.ts +10 -0
  6. package/dist/discovery/http.d.ts.map +1 -0
  7. package/dist/discovery/http.js +35 -0
  8. package/dist/discovery/http.js.map +1 -0
  9. package/dist/discovery/index.d.ts +9 -9
  10. package/dist/discovery/index.d.ts.map +1 -1
  11. package/dist/discovery/index.js +27 -55
  12. package/dist/discovery/index.js.map +1 -1
  13. package/dist/graphql/index.d.ts +8 -76
  14. package/dist/graphql/index.d.ts.map +1 -1
  15. package/dist/graphql/index.js +17 -197
  16. package/dist/graphql/index.js.map +1 -1
  17. package/dist/graphql/introspection.d.ts +5 -0
  18. package/dist/graphql/introspection.d.ts.map +1 -0
  19. package/dist/graphql/introspection.js +136 -0
  20. package/dist/graphql/introspection.js.map +1 -0
  21. package/dist/graphql/operation.d.ts +5 -0
  22. package/dist/graphql/operation.d.ts.map +1 -0
  23. package/dist/graphql/operation.js +39 -0
  24. package/dist/graphql/operation.js.map +1 -0
  25. package/dist/graphql/types.d.ts +71 -0
  26. package/dist/graphql/types.d.ts.map +1 -0
  27. package/dist/graphql/types.js +2 -0
  28. package/dist/graphql/types.js.map +1 -0
  29. package/dist/index.d.ts +1 -17
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -34
  32. package/dist/index.js.map +1 -1
  33. package/dist/openapi/endpoints.d.ts +5 -0
  34. package/dist/openapi/endpoints.d.ts.map +1 -0
  35. package/dist/openapi/endpoints.js +77 -0
  36. package/dist/openapi/endpoints.js.map +1 -0
  37. package/dist/openapi/index.d.ts +7 -87
  38. package/dist/openapi/index.d.ts.map +1 -1
  39. package/dist/openapi/index.js +8 -267
  40. package/dist/openapi/index.js.map +1 -1
  41. package/dist/openapi/operation.d.ts +5 -0
  42. package/dist/openapi/operation.d.ts.map +1 -0
  43. package/dist/openapi/operation.js +141 -0
  44. package/dist/openapi/operation.js.map +1 -0
  45. package/dist/openapi/schema.d.ts +5 -0
  46. package/dist/openapi/schema.d.ts.map +1 -0
  47. package/dist/openapi/schema.js +84 -0
  48. package/dist/openapi/schema.js.map +1 -0
  49. package/dist/openapi/types.d.ts +82 -0
  50. package/dist/openapi/types.d.ts.map +1 -0
  51. package/dist/openapi/types.js +2 -0
  52. package/dist/openapi/types.js.map +1 -0
  53. package/dist/packageInfo.d.ts +18 -0
  54. package/dist/packageInfo.d.ts.map +1 -0
  55. package/dist/packageInfo.js +30 -0
  56. package/dist/packageInfo.js.map +1 -0
  57. package/dist/rest/index.d.ts +5 -5
  58. package/dist/rest/index.d.ts.map +1 -1
  59. package/dist/rest/index.js +49 -48
  60. package/dist/rest/index.js.map +1 -1
  61. package/dist/test-runner/index.d.ts +2 -57
  62. package/dist/test-runner/index.d.ts.map +1 -1
  63. package/dist/test-runner/index.js +58 -160
  64. package/dist/test-runner/index.js.map +1 -1
  65. package/dist/test-runner/response.d.ts +3 -0
  66. package/dist/test-runner/response.d.ts.map +1 -0
  67. package/dist/test-runner/response.js +29 -0
  68. package/dist/test-runner/response.js.map +1 -0
  69. package/dist/test-runner/result.d.ts +6 -0
  70. package/dist/test-runner/result.d.ts.map +1 -0
  71. package/dist/test-runner/result.js +17 -0
  72. package/dist/test-runner/result.js.map +1 -0
  73. package/dist/test-runner/serialization.d.ts +12 -0
  74. package/dist/test-runner/serialization.d.ts.map +1 -0
  75. package/dist/test-runner/serialization.js +99 -0
  76. package/dist/test-runner/serialization.js.map +1 -0
  77. package/dist/test-runner/types.d.ts +58 -0
  78. package/dist/test-runner/types.d.ts.map +1 -0
  79. package/dist/test-runner/types.js +2 -0
  80. package/dist/test-runner/types.js.map +1 -0
  81. package/package.json +5 -5
  82. package/src/ankh.provider.ts +1 -1
  83. package/src/api-phase1.contract.test.ts +24 -0
  84. package/src/discovery/http.ts +38 -0
  85. package/src/discovery/index.test.ts +76 -75
  86. package/src/discovery/index.ts +49 -72
  87. package/src/graphql/index.test.ts +10 -11
  88. package/src/graphql/index.ts +35 -315
  89. package/src/graphql/introspection.ts +163 -0
  90. package/src/graphql/operation.ts +60 -0
  91. package/src/graphql/types.ts +79 -0
  92. package/src/index.test.ts +21 -13
  93. package/src/index.ts +1 -54
  94. package/src/openapi/endpoints.ts +124 -0
  95. package/src/openapi/index.test.ts +47 -47
  96. package/src/openapi/index.ts +12 -467
  97. package/src/openapi/operation.ts +231 -0
  98. package/src/openapi/schema.ts +127 -0
  99. package/src/openapi/types.ts +93 -0
  100. package/src/packageInfo.ts +50 -0
  101. package/src/rest/index.test.ts +17 -14
  102. package/src/rest/index.ts +67 -61
  103. package/src/test-runner/index.test.ts +153 -120
  104. package/src/test-runner/index.ts +97 -299
  105. package/src/test-runner/response.ts +32 -0
  106. package/src/test-runner/result.ts +35 -0
  107. package/src/test-runner/serialization.ts +151 -0
  108. package/src/test-runner/types.ts +81 -0
  109. package/dist/generated-api/index.d.ts +0 -10
  110. package/dist/generated-api/index.d.ts.map +0 -1
  111. package/dist/generated-api/index.js +0 -274
  112. package/dist/generated-api/index.js.map +0 -1
  113. package/dist/generated-api/validateGeneratedApiCollection.d.ts +0 -3
  114. package/dist/generated-api/validateGeneratedApiCollection.d.ts.map +0 -1
  115. package/dist/generated-api/validateGeneratedApiCollection.js +0 -35
  116. package/dist/generated-api/validateGeneratedApiCollection.js.map +0 -1
  117. package/src/generated-api/index.test.ts +0 -173
  118. package/src/generated-api/index.ts +0 -414
  119. package/src/generated-api/validateGeneratedApiCollection.test.ts +0 -91
  120. package/src/generated-api/validateGeneratedApiCollection.ts +0 -76
@@ -1,87 +1,31 @@
1
1
  import type {
2
- CredentialRef,
3
- DataContractValue,
2
+ ApiDefinition,
4
3
  DataEndpointConfig,
5
4
  DataOperationConfig,
6
- DataOperationParameter,
7
- DataSourceConfig,
8
5
  DataSourceDiagnostic,
9
- EndpointId,
10
- OperationId,
11
6
  } from '@ankhorage/contracts/data';
12
7
 
13
- export type EndpointTestInputValues = Readonly<Record<string, DataContractValue>>;
14
- export type EndpointTestHeaders = Readonly<Record<string, string>>;
15
-
16
- export interface EndpointTestCredential {
17
- readonly headers?: EndpointTestHeaders;
18
- readonly query?: EndpointTestInputValues;
19
- }
20
-
21
- export type EndpointTestCredentialResolver = (
22
- credential: CredentialRef,
23
- ) => EndpointTestCredential | Promise<EndpointTestCredential | undefined> | undefined;
24
-
25
- export interface EndpointTestFetchInit {
26
- readonly method: string;
27
- readonly headers: EndpointTestHeaders;
28
- readonly body?: string;
29
- }
30
-
31
- export interface EndpointTestFetchResponse {
32
- readonly status: number;
33
- readonly headers?: EndpointTestHeaders;
34
- text(): Promise<string>;
35
- }
36
-
37
- export type EndpointTestFetch = (
38
- url: string,
39
- init: EndpointTestFetchInit,
40
- ) => Promise<EndpointTestFetchResponse>;
41
-
42
- export interface EndpointTestInput {
43
- readonly dataSource: DataSourceConfig;
44
- readonly endpointId: EndpointId;
45
- readonly operationId: OperationId;
46
- readonly values?: EndpointTestInputValues;
47
- readonly dryRun?: boolean;
48
- readonly fetch?: EndpointTestFetch;
49
- readonly credentialResolver?: EndpointTestCredentialResolver;
50
- }
51
-
52
- export interface EndpointTestRequestDiagnostic {
53
- readonly dataSourceId: string;
54
- readonly endpointId: EndpointId;
55
- readonly operationId: OperationId;
56
- readonly url: string;
57
- readonly method: string;
58
- readonly headers: EndpointTestHeaders;
59
- readonly body?: string;
60
- readonly dryRun: boolean;
61
- }
62
-
63
- export interface EndpointTestResponseDiagnostic {
64
- readonly status: number;
65
- readonly ok: boolean;
66
- readonly headers?: EndpointTestHeaders;
67
- readonly bodyText?: string;
68
- readonly parsedBody?: DataContractValue;
69
- }
8
+ import { createResponseDiagnostic } from './response';
9
+ import { createDiagnostic, createNetworkErrorMessage, failure } from './result';
10
+ import {
11
+ appendQuery,
12
+ collectHeaders,
13
+ collectQuery,
14
+ createHttpBody,
15
+ getRecordValue,
16
+ getStringMetadataValue,
17
+ getStringValue,
18
+ interpolatePath,
19
+ joinUrl,
20
+ } from './serialization';
21
+ import type {
22
+ EndpointTestCredential,
23
+ EndpointTestInput,
24
+ EndpointTestRequestDiagnostic,
25
+ EndpointTestResult,
26
+ } from './types';
70
27
 
71
- export type EndpointTestResult =
72
- | {
73
- readonly ok: true;
74
- readonly request: EndpointTestRequestDiagnostic;
75
- readonly response?: EndpointTestResponseDiagnostic;
76
- readonly data?: DataContractValue;
77
- readonly diagnostics: readonly DataSourceDiagnostic[];
78
- }
79
- | {
80
- readonly ok: false;
81
- readonly request?: EndpointTestRequestDiagnostic;
82
- readonly response?: EndpointTestResponseDiagnostic;
83
- readonly diagnostics: readonly DataSourceDiagnostic[];
84
- };
28
+ export type * from './types';
85
29
 
86
30
  interface EndpointOperationSelection {
87
31
  readonly endpoint: DataEndpointConfig;
@@ -109,26 +53,7 @@ export async function testEndpoint(input: EndpointTestInput): Promise<EndpointTe
109
53
  headers: built.request.headers,
110
54
  body: built.request.body,
111
55
  });
112
- const responseDiagnostic = await createResponseDiagnostic(response);
113
- const diagnostics = [...built.diagnostics];
114
- if (!responseDiagnostic.ok) {
115
- diagnostics.push(
116
- createDiagnostic(
117
- input,
118
- 'http-error',
119
- `Endpoint test request returned HTTP status ${responseDiagnostic.status}.`,
120
- ),
121
- );
122
- }
123
- return responseDiagnostic.ok
124
- ? {
125
- ok: true,
126
- request: built.request,
127
- response: responseDiagnostic,
128
- data: responseDiagnostic.parsedBody,
129
- diagnostics,
130
- }
131
- : failure(built.request, diagnostics, responseDiagnostic);
56
+ return createEndpointTestResult(input, built, await createResponseDiagnostic(response));
132
57
  } catch (error) {
133
58
  return failure(built.request, [
134
59
  ...built.diagnostics,
@@ -140,38 +65,64 @@ export async function testEndpoint(input: EndpointTestInput): Promise<EndpointTe
140
65
  export async function buildEndpointTestRequest(
141
66
  input: EndpointTestInput,
142
67
  ): Promise<EndpointTestResult> {
68
+ if (input.api.origin === 'internal') {
69
+ return failure(undefined, [
70
+ createDiagnostic(input, 'invalid-config', 'Internal APIs are not executable in API phase 1.'),
71
+ ]);
72
+ }
73
+
143
74
  const selection = selectEndpointOperation(input);
144
75
  if (selection === undefined) {
145
76
  return failure(undefined, [
146
77
  createDiagnostic(input, 'missing-operation', 'Endpoint or operation could not be found.'),
147
78
  ]);
148
79
  }
149
-
150
- if (selection.operation.protocol === 'database') {
80
+ if (selection.operation.protocol !== 'http' && selection.operation.protocol !== 'graphql') {
151
81
  return failure(undefined, [
152
82
  createDiagnostic(
153
83
  input,
154
84
  'invalid-config',
155
- 'Database-backed generated API operations require an adapter executor and cannot be tested through the HTTP endpoint runner.',
85
+ `API operation protocol '${selection.operation.protocol}' is not supported.`,
156
86
  ),
157
87
  ]);
158
88
  }
159
89
 
160
90
  const diagnostics: DataSourceDiagnostic[] = [];
161
91
  const credential = await resolveEndpointCredential(input, selection, diagnostics);
162
- const request =
163
- selection.operation.protocol === 'graphql'
164
- ? buildGraphQlRequest(input, selection, credential, diagnostics)
165
- : buildHttpRequest(input, selection, credential, diagnostics);
166
-
92
+ const request = buildProtocolRequest(input, selection, credential, diagnostics);
167
93
  if (request === undefined || diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
168
94
  return failure(request, diagnostics);
169
95
  }
170
96
  return { ok: true, request, diagnostics };
171
97
  }
172
98
 
99
+ function createEndpointTestResult(
100
+ input: EndpointTestInput,
101
+ built: Extract<EndpointTestResult, { readonly ok: true }>,
102
+ response: Awaited<ReturnType<typeof createResponseDiagnostic>>,
103
+ ): EndpointTestResult {
104
+ const diagnostics = [...built.diagnostics];
105
+ if (!response.ok) {
106
+ diagnostics.push(
107
+ createDiagnostic(
108
+ input,
109
+ 'http-error',
110
+ `Endpoint test request returned HTTP status ${response.status}.`,
111
+ ),
112
+ );
113
+ return failure(built.request, diagnostics, response);
114
+ }
115
+ return {
116
+ ok: true,
117
+ request: built.request,
118
+ response,
119
+ data: response.parsedBody,
120
+ diagnostics,
121
+ };
122
+ }
123
+
173
124
  function selectEndpointOperation(input: EndpointTestInput): EndpointOperationSelection | undefined {
174
- const endpoint = input.dataSource.endpoints[input.endpointId];
125
+ const endpoint = input.api.endpoints[input.endpointId];
175
126
  const operation = endpoint?.operations[input.operationId];
176
127
  return endpoint === undefined || operation === undefined ? undefined : { endpoint, operation };
177
128
  }
@@ -182,7 +133,7 @@ async function resolveEndpointCredential(
182
133
  diagnostics: DataSourceDiagnostic[],
183
134
  ): Promise<EndpointTestCredential | undefined> {
184
135
  const ref =
185
- selection.operation.credential ?? selection.endpoint.credential ?? input.dataSource.credential;
136
+ selection.operation.credential ?? selection.endpoint.credential ?? input.api.credential;
186
137
  if (ref === undefined) return undefined;
187
138
  if (input.credentialResolver === undefined) {
188
139
  diagnostics.push(
@@ -203,22 +154,31 @@ async function resolveEndpointCredential(
203
154
  return credential;
204
155
  }
205
156
 
157
+ function buildProtocolRequest(
158
+ input: EndpointTestInput,
159
+ selection: EndpointOperationSelection,
160
+ credential: EndpointTestCredential | undefined,
161
+ diagnostics: DataSourceDiagnostic[],
162
+ ): EndpointTestRequestDiagnostic | undefined {
163
+ return selection.operation.protocol === 'graphql'
164
+ ? buildGraphQlRequest(input, selection, credential, diagnostics)
165
+ : buildHttpRequest(input, selection, credential, diagnostics);
166
+ }
167
+
206
168
  function buildHttpRequest(
207
169
  input: EndpointTestInput,
208
170
  selection: EndpointOperationSelection,
209
171
  credential: EndpointTestCredential | undefined,
210
172
  diagnostics: DataSourceDiagnostic[],
211
173
  ): EndpointTestRequestDiagnostic | undefined {
212
- const baseUrl = getDataSourceBaseUrl(input.dataSource, selection.endpoint);
174
+ const baseUrl = getRestBaseUrl(input.api, selection.endpoint);
213
175
  const path = selection.operation.path ?? selection.endpoint.path;
214
- if (!baseUrl) {
215
- diagnostics.push(
216
- createDiagnostic(input, 'invalid-config', 'HTTP endpoint requires a base URL.'),
217
- );
218
- return undefined;
219
- }
220
- if (path === undefined) {
221
- diagnostics.push(createDiagnostic(input, 'invalid-config', 'HTTP operation requires a path.'));
176
+ if (baseUrl === undefined || path === undefined) {
177
+ const message =
178
+ baseUrl === undefined
179
+ ? 'HTTP endpoint requires a base URL.'
180
+ : 'HTTP operation requires a path.';
181
+ diagnostics.push(createDiagnostic(input, 'invalid-config', message));
222
182
  return undefined;
223
183
  }
224
184
 
@@ -226,17 +186,25 @@ function buildHttpRequest(
226
186
  const parameters = selection.operation.request?.parameters ?? [];
227
187
  const resolvedPath = interpolatePath(path, parameters, values, input, diagnostics);
228
188
  const query = collectQuery(parameters, values, credential?.query);
229
- const body = createHttpBody(selection.operation, values);
230
- const headers: Record<string, string> = { ...(credential?.headers ?? {}) };
231
- if (body !== undefined) headers['content-type'] = headers['content-type'] ?? 'application/json';
232
-
189
+ const headers = collectHeaders(parameters, values, credential?.headers);
190
+ const body = createHttpBody(selection.operation, parameters, values);
191
+ const requestHeaders =
192
+ body === undefined
193
+ ? headers
194
+ : {
195
+ ...headers,
196
+ 'content-type':
197
+ headers['content-type'] ??
198
+ selection.operation.request?.contentType ??
199
+ 'application/json',
200
+ };
233
201
  return {
234
- dataSourceId: input.dataSource.id,
202
+ apiId: input.api.id,
235
203
  endpointId: input.endpointId,
236
204
  operationId: input.operationId,
237
205
  url: appendQuery(joinUrl(baseUrl, resolvedPath), query),
238
206
  method: selection.operation.method ?? 'GET',
239
- headers,
207
+ headers: requestHeaders,
240
208
  body,
241
209
  dryRun: input.dryRun === true,
242
210
  };
@@ -248,8 +216,8 @@ function buildGraphQlRequest(
248
216
  credential: EndpointTestCredential | undefined,
249
217
  diagnostics: DataSourceDiagnostic[],
250
218
  ): EndpointTestRequestDiagnostic | undefined {
251
- const endpointUrl = getGraphQlEndpointUrl(input.dataSource, selection.endpoint);
252
- if (!endpointUrl) {
219
+ const endpointUrl = getGraphQlEndpointUrl(input.api, selection.endpoint);
220
+ if (endpointUrl === undefined) {
253
221
  diagnostics.push(
254
222
  createDiagnostic(input, 'invalid-config', 'GraphQL endpoint requires an endpoint URL.'),
255
223
  );
@@ -268,7 +236,7 @@ function buildGraphQlRequest(
268
236
 
269
237
  const variables = getRecordValue(input.values, 'variables');
270
238
  return {
271
- dataSourceId: input.dataSource.id,
239
+ apiId: input.api.id,
272
240
  endpointId: input.endpointId,
273
241
  operationId: input.operationId,
274
242
  url: appendQuery(endpointUrl, credential?.query ?? {}),
@@ -279,185 +247,15 @@ function buildGraphQlRequest(
279
247
  };
280
248
  }
281
249
 
282
- function getDataSourceBaseUrl(
283
- source: DataSourceConfig,
284
- endpoint: DataEndpointConfig,
285
- ): string | undefined {
250
+ function getRestBaseUrl(api: ApiDefinition, endpoint: DataEndpointConfig): string | undefined {
286
251
  if (endpoint.baseUrl !== undefined) return endpoint.baseUrl;
287
- if (source.kind !== 'api' || source.origin !== 'external') return undefined;
288
- return source.protocol === 'rest' ? source.baseUrl : source.endpointUrl;
252
+ return api.origin === 'external' && api.protocol === 'rest' ? api.baseUrl : undefined;
289
253
  }
290
254
 
291
255
  function getGraphQlEndpointUrl(
292
- source: DataSourceConfig,
256
+ api: ApiDefinition,
293
257
  endpoint: DataEndpointConfig,
294
258
  ): string | undefined {
295
259
  if (endpoint.baseUrl !== undefined) return endpoint.baseUrl;
296
- return source.kind === 'api' && source.origin === 'external' && source.protocol === 'graphql'
297
- ? source.endpointUrl
298
- : undefined;
299
- }
300
-
301
- function interpolatePath(
302
- path: string,
303
- parameters: readonly DataOperationParameter[],
304
- values: EndpointTestInputValues,
305
- input: EndpointTestInput,
306
- diagnostics: DataSourceDiagnostic[],
307
- ): string {
308
- let next = path;
309
- for (const parameter of parameters.filter((item) => item.location === 'path')) {
310
- const value = values[parameter.name] ?? parameter.default;
311
- if (value === undefined) {
312
- diagnostics.push(
313
- createDiagnostic(
314
- input,
315
- 'invalid-config',
316
- `Missing required path parameter '${parameter.name}'.`,
317
- ),
318
- );
319
- continue;
320
- }
321
- const encoded = encodeURIComponent(serializeEndpointUrlValue(value));
322
- next = next
323
- .replaceAll(`{${parameter.name}}`, encoded)
324
- .replaceAll(`:${parameter.name}`, encoded);
325
- }
326
- return next;
327
- }
328
-
329
- function collectQuery(
330
- parameters: readonly DataOperationParameter[],
331
- values: EndpointTestInputValues,
332
- credentialQuery: EndpointTestInputValues | undefined,
333
- ): EndpointTestInputValues {
334
- const query: Record<string, DataContractValue> = { ...(credentialQuery ?? {}) };
335
- for (const parameter of parameters) {
336
- if (parameter.location !== 'query') continue;
337
- const value = values[parameter.name] ?? parameter.default;
338
- if (value !== undefined) query[parameter.name] = value;
339
- }
340
- return query;
341
- }
342
-
343
- function createHttpBody(
344
- operation: DataOperationConfig,
345
- values: EndpointTestInputValues,
346
- ): string | undefined {
347
- if (operation.method === 'GET' || operation.method === 'HEAD') return undefined;
348
- return values.body === undefined ? undefined : JSON.stringify(values.body);
349
- }
350
-
351
- async function createResponseDiagnostic(
352
- response: EndpointTestFetchResponse,
353
- ): Promise<EndpointTestResponseDiagnostic> {
354
- const bodyText = await response.text();
355
- return {
356
- status: response.status,
357
- ok: response.status >= 200 && response.status < 300,
358
- headers: response.headers,
359
- bodyText,
360
- parsedBody: parseResponseBody(bodyText),
361
- };
362
- }
363
-
364
- function parseResponseBody(bodyText: string): DataContractValue | undefined {
365
- if (bodyText.trim().length === 0) return undefined;
366
- try {
367
- const parsed: unknown = JSON.parse(bodyText);
368
- return isDataContractValue(parsed) ? parsed : { raw: bodyText };
369
- } catch {
370
- return { raw: bodyText };
371
- }
372
- }
373
-
374
- function appendQuery(url: string, query: EndpointTestInputValues): string {
375
- const entries = Object.entries(query);
376
- if (entries.length === 0) return url;
377
- const search = entries
378
- .map(
379
- ([key, value]) =>
380
- `${encodeURIComponent(key)}=${encodeURIComponent(serializeEndpointUrlValue(value))}`,
381
- )
382
- .join('&');
383
- return `${url}${url.includes('?') ? '&' : '?'}${search}`;
384
- }
385
-
386
- function serializeEndpointUrlValue(value: DataContractValue): string {
387
- if (typeof value === 'string') return value;
388
- if (typeof value === 'number' || typeof value === 'boolean') return value.toString();
389
- if (value === null) return '';
390
- return JSON.stringify(value);
391
- }
392
-
393
- function joinUrl(baseUrl: string, path: string): string {
394
- const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
395
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
396
- return `${normalizedBaseUrl}${normalizedPath}`;
397
- }
398
-
399
- function getStringValue(
400
- values: EndpointTestInputValues | undefined,
401
- key: string,
402
- ): string | undefined {
403
- const value = values?.[key];
404
- return typeof value === 'string' ? value : undefined;
405
- }
406
-
407
- function getRecordValue(
408
- values: EndpointTestInputValues | undefined,
409
- key: string,
410
- ): DataContractValue | undefined {
411
- const value = values?.[key];
412
- return isDataContractValue(value) ? value : undefined;
413
- }
414
-
415
- function getStringMetadataValue(
416
- metadata: DataContractValue | undefined,
417
- key: string,
418
- ): string | undefined {
419
- if (!isDataContractRecord(metadata)) return undefined;
420
- const value = metadata[key];
421
- return typeof value === 'string' ? value : undefined;
422
- }
423
-
424
- function createDiagnostic(
425
- input: EndpointTestInput,
426
- code: string,
427
- message: string,
428
- ): DataSourceDiagnostic {
429
- return {
430
- code,
431
- dataSourceId: input.dataSource.id,
432
- endpointId: input.endpointId,
433
- operationId: input.operationId,
434
- message,
435
- severity: code === 'missing-schema' ? 'info' : 'error',
436
- };
437
- }
438
-
439
- function failure(
440
- request: EndpointTestRequestDiagnostic | undefined,
441
- diagnostics: readonly DataSourceDiagnostic[],
442
- response?: EndpointTestResponseDiagnostic,
443
- ): EndpointTestResult {
444
- return { ok: false, request, response, diagnostics };
445
- }
446
-
447
- function createNetworkErrorMessage(error: unknown): string {
448
- return error instanceof Error ? error.message : 'Endpoint test request failed.';
449
- }
450
-
451
- function isDataContractValue(value: unknown): value is DataContractValue {
452
- if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return true;
453
- if (Array.isArray(value)) return value.every(isDataContractValue);
454
- return typeof value === 'object' && Object.values(value).every(isDataContractValue);
455
- }
456
-
457
- function isDataContractRecord(
458
- value: DataContractValue | undefined,
459
- ): value is Record<string, DataContractValue> {
460
- return (
461
- value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value)
462
- );
260
+ return api.origin === 'external' && api.protocol === 'graphql' ? api.endpointUrl : undefined;
463
261
  }
@@ -0,0 +1,32 @@
1
+ import type { DataContractValue } from '@ankhorage/contracts/data';
2
+
3
+ import type { EndpointTestFetchResponse, EndpointTestResponseDiagnostic } from './types';
4
+
5
+ export async function createResponseDiagnostic(
6
+ response: EndpointTestFetchResponse,
7
+ ): Promise<EndpointTestResponseDiagnostic> {
8
+ const bodyText = await response.text();
9
+ return {
10
+ status: response.status,
11
+ ok: response.status >= 200 && response.status < 300,
12
+ headers: response.headers,
13
+ bodyText,
14
+ parsedBody: parseResponseBody(bodyText),
15
+ };
16
+ }
17
+
18
+ function parseResponseBody(bodyText: string): DataContractValue | undefined {
19
+ if (bodyText.trim().length === 0) return undefined;
20
+ try {
21
+ const parsed: unknown = JSON.parse(bodyText);
22
+ return isDataContractValue(parsed) ? parsed : { raw: bodyText };
23
+ } catch {
24
+ return { raw: bodyText };
25
+ }
26
+ }
27
+
28
+ function isDataContractValue(value: unknown): value is DataContractValue {
29
+ if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return true;
30
+ if (Array.isArray(value)) return value.every(isDataContractValue);
31
+ return typeof value === 'object' && Object.values(value).every(isDataContractValue);
32
+ }
@@ -0,0 +1,35 @@
1
+ import type { DataSourceDiagnostic } from '@ankhorage/contracts/data';
2
+
3
+ import type {
4
+ EndpointTestInput,
5
+ EndpointTestRequestDiagnostic,
6
+ EndpointTestResponseDiagnostic,
7
+ EndpointTestResult,
8
+ } from './types';
9
+
10
+ export function createDiagnostic(
11
+ input: EndpointTestInput,
12
+ code: string,
13
+ message: string,
14
+ ): DataSourceDiagnostic {
15
+ return {
16
+ code,
17
+ apiId: input.api.id,
18
+ endpointId: input.endpointId,
19
+ operationId: input.operationId,
20
+ message,
21
+ severity: code === 'missing-schema' ? 'info' : 'error',
22
+ };
23
+ }
24
+
25
+ export function failure(
26
+ request: EndpointTestRequestDiagnostic | undefined,
27
+ diagnostics: readonly DataSourceDiagnostic[],
28
+ response?: EndpointTestResponseDiagnostic,
29
+ ): EndpointTestResult {
30
+ return { ok: false, request, response, diagnostics };
31
+ }
32
+
33
+ export function createNetworkErrorMessage(error: unknown): string {
34
+ return error instanceof Error ? error.message : 'Endpoint test request failed.';
35
+ }