@ankhorage/data-sources 0.3.0 → 0.5.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @ankhorage/data-sources
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5a52064: Add a headless endpoint test runner with dry-run request building, injected fetch execution, credential resolution, response parsing, and structured diagnostics.
8
+
9
+ ## 0.4.0
10
+
11
+ ### Minor Changes
12
+
13
+ - bc25acb: Add a headless endpoint test runner with dry-run request building, injected fetch execution, credential resolution, response parsing, and structured diagnostics.
14
+
3
15
  ## 0.3.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @ankhorage/data-sources
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v0.2.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v0.3.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
7
7
 
8
8
  Provider-neutral data source and endpoint system for APIs, databases, and runtime bindings.
9
9
 
@@ -43,6 +43,8 @@ graph TD
43
43
  package__ankhorage_data_sources -.-> module_src_openapi_index_ts
44
44
  module_src_rest_index_ts["src/rest/index.ts"]
45
45
  package__ankhorage_data_sources -.-> module_src_rest_index_ts
46
+ module_src_test_runner_index_ts["src/test-runner/index.ts"]
47
+ package__ankhorage_data_sources -.-> module_src_test_runner_index_ts
46
48
  ```
47
49
 
48
50
  </details>
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from './graphql';
3
3
  export * from './managed-api';
4
4
  export * from './openapi';
5
5
  export * from './rest';
6
+ export * from './test-runner';
6
7
  export declare const DATA_SOURCES_PACKAGE_NAME: "@ankhorage/data-sources";
7
8
  export declare const SUPPORTED_DATA_SOURCE_KINDS: readonly ["database", "graphql", "managed-api", "openapi", "rest"];
8
9
  export interface DataSourcesPackageInfo {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAElF,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AAEvB,eAAO,MAAM,yBAAyB,EAAG,yBAAkC,CAAC;AAE5E,eAAO,MAAM,2BAA2B,oEAMM,CAAC;AAE/C,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,OAAO,yBAAyB,CAAC;IACvD,QAAQ,CAAC,cAAc,EAAE,OAAO,2BAA2B,CAAC;CAC7D;AAED,wBAAgB,yBAAyB,IAAI,sBAAsB,CAKlE;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,cAAc,CAE9E;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG,cAAc,CAE1E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAElF,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAE9B,eAAO,MAAM,yBAAyB,EAAG,yBAAkC,CAAC;AAE5E,eAAO,MAAM,2BAA2B,oEAMM,CAAC;AAE/C,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,OAAO,yBAAyB,CAAC;IACvD,QAAQ,CAAC,cAAc,EAAE,OAAO,2BAA2B,CAAC;CAC7D;AAED,wBAAgB,yBAAyB,IAAI,sBAAsB,CAKlE;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,cAAc,CAE9E;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG,cAAc,CAE1E"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from './graphql';
2
2
  export * from './managed-api';
3
3
  export * from './openapi';
4
4
  export * from './rest';
5
+ export * from './test-runner';
5
6
  export const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources';
6
7
  export const SUPPORTED_DATA_SOURCE_KINDS = [
7
8
  'database',
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AAEvB,MAAM,CAAC,MAAM,yBAAyB,GAAG,yBAAkC,CAAC;AAE5E,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,UAAU;IACV,SAAS;IACT,aAAa;IACb,SAAS;IACT,MAAM;CACsC,CAAC;AAO/C,MAAM,UAAU,yBAAyB;IACvC,OAAO;QACL,WAAW,EAAE,yBAAyB;QACtC,cAAc,EAAE,2BAA2B;KAC5C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAwB;IACxD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC","sourcesContent":["import type { DataSourceConfig, DataSourceKind } from '@ankhorage/contracts/data';\n\nexport * from './graphql';\nexport * from './managed-api';\nexport * from './openapi';\nexport * from './rest';\n\nexport const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;\n\nexport const SUPPORTED_DATA_SOURCE_KINDS = [\n 'database',\n 'graphql',\n 'managed-api',\n 'openapi',\n 'rest',\n] as const satisfies readonly DataSourceKind[];\n\nexport interface DataSourcesPackageInfo {\n readonly packageName: typeof DATA_SOURCES_PACKAGE_NAME;\n readonly supportedKinds: typeof SUPPORTED_DATA_SOURCE_KINDS;\n}\n\nexport function getDataSourcesPackageInfo(): DataSourcesPackageInfo {\n return {\n packageName: DATA_SOURCES_PACKAGE_NAME,\n supportedKinds: SUPPORTED_DATA_SOURCE_KINDS,\n };\n}\n\nexport function isSupportedDataSourceKind(kind: string): kind is DataSourceKind {\n return SUPPORTED_DATA_SOURCE_KINDS.some((supportedKind) => supportedKind === kind);\n}\n\nexport function getDataSourceKind(source: DataSourceConfig): DataSourceKind {\n return source.kind;\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAE9B,MAAM,CAAC,MAAM,yBAAyB,GAAG,yBAAkC,CAAC;AAE5E,MAAM,CAAC,MAAM,2BAA2B,GAAG;IACzC,UAAU;IACV,SAAS;IACT,aAAa;IACb,SAAS;IACT,MAAM;CACsC,CAAC;AAO/C,MAAM,UAAU,yBAAyB;IACvC,OAAO;QACL,WAAW,EAAE,yBAAyB;QACtC,cAAc,EAAE,2BAA2B;KAC5C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAwB;IACxD,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC","sourcesContent":["import type { DataSourceConfig, DataSourceKind } from '@ankhorage/contracts/data';\n\nexport * from './graphql';\nexport * from './managed-api';\nexport * from './openapi';\nexport * from './rest';\nexport * from './test-runner';\n\nexport const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;\n\nexport const SUPPORTED_DATA_SOURCE_KINDS = [\n 'database',\n 'graphql',\n 'managed-api',\n 'openapi',\n 'rest',\n] as const satisfies readonly DataSourceKind[];\n\nexport interface DataSourcesPackageInfo {\n readonly packageName: typeof DATA_SOURCES_PACKAGE_NAME;\n readonly supportedKinds: typeof SUPPORTED_DATA_SOURCE_KINDS;\n}\n\nexport function getDataSourcesPackageInfo(): DataSourcesPackageInfo {\n return {\n packageName: DATA_SOURCES_PACKAGE_NAME,\n supportedKinds: SUPPORTED_DATA_SOURCE_KINDS,\n };\n}\n\nexport function isSupportedDataSourceKind(kind: string): kind is DataSourceKind {\n return SUPPORTED_DATA_SOURCE_KINDS.some((supportedKind) => supportedKind === kind);\n}\n\nexport function getDataSourceKind(source: DataSourceConfig): DataSourceKind {\n return source.kind;\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import type { CredentialRef, DataContractValue, DataSourceConfig, DataSourceDiagnostic, EndpointId, OperationId } from '@ankhorage/contracts/data';
2
+ export type EndpointTestInputValues = Readonly<Record<string, DataContractValue>>;
3
+ export type EndpointTestHeaders = Readonly<Record<string, string>>;
4
+ export interface EndpointTestCredential {
5
+ readonly headers?: EndpointTestHeaders;
6
+ readonly query?: EndpointTestInputValues;
7
+ }
8
+ export type EndpointTestCredentialResolver = (credential: CredentialRef) => EndpointTestCredential | Promise<EndpointTestCredential | undefined> | undefined;
9
+ export interface EndpointTestFetchInit {
10
+ readonly method: string;
11
+ readonly headers: EndpointTestHeaders;
12
+ readonly body?: string;
13
+ }
14
+ export interface EndpointTestFetchResponse {
15
+ readonly status: number;
16
+ readonly headers?: EndpointTestHeaders;
17
+ text(): Promise<string>;
18
+ }
19
+ export type EndpointTestFetch = (url: string, init: EndpointTestFetchInit) => Promise<EndpointTestFetchResponse>;
20
+ export interface EndpointTestInput {
21
+ readonly dataSource: DataSourceConfig;
22
+ readonly endpointId: EndpointId;
23
+ readonly operationId: OperationId;
24
+ readonly values?: EndpointTestInputValues;
25
+ readonly dryRun?: boolean;
26
+ readonly fetch?: EndpointTestFetch;
27
+ readonly credentialResolver?: EndpointTestCredentialResolver;
28
+ }
29
+ export interface EndpointTestRequestDiagnostic {
30
+ readonly dataSourceId: string;
31
+ readonly endpointId: EndpointId;
32
+ readonly operationId: OperationId;
33
+ readonly url: string;
34
+ readonly method: string;
35
+ readonly headers: EndpointTestHeaders;
36
+ readonly body?: string;
37
+ readonly dryRun: boolean;
38
+ }
39
+ export interface EndpointTestResponseDiagnostic {
40
+ readonly status: number;
41
+ readonly ok: boolean;
42
+ readonly headers?: EndpointTestHeaders;
43
+ readonly bodyText?: string;
44
+ readonly parsedBody?: DataContractValue;
45
+ }
46
+ export type EndpointTestResult = {
47
+ readonly ok: true;
48
+ readonly request: EndpointTestRequestDiagnostic;
49
+ readonly response?: EndpointTestResponseDiagnostic;
50
+ readonly data?: DataContractValue;
51
+ readonly diagnostics: readonly DataSourceDiagnostic[];
52
+ } | {
53
+ readonly ok: false;
54
+ readonly request?: EndpointTestRequestDiagnostic;
55
+ readonly response?: EndpointTestResponseDiagnostic;
56
+ readonly diagnostics: readonly DataSourceDiagnostic[];
57
+ };
58
+ export declare function testEndpoint(input: EndpointTestInput): Promise<EndpointTestResult>;
59
+ export declare function buildEndpointTestRequest(input: EndpointTestInput): Promise<EndpointTestResult>;
60
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-runner/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,307 @@
1
+ export async function testEndpoint(input) {
2
+ const built = await buildEndpointTestRequest(input);
3
+ if (!built.ok)
4
+ return built;
5
+ if (input.dryRun === true)
6
+ return built;
7
+ if (input.fetch === undefined) {
8
+ return {
9
+ ok: false,
10
+ request: built.request,
11
+ diagnostics: [
12
+ ...built.diagnostics,
13
+ createDiagnostic(input, 'invalid-config', 'Endpoint test runner requires an injected fetch function.'),
14
+ ],
15
+ };
16
+ }
17
+ try {
18
+ const response = await input.fetch(built.request.url, {
19
+ method: built.request.method,
20
+ headers: built.request.headers,
21
+ body: built.request.body,
22
+ });
23
+ const responseDiagnostic = await createResponseDiagnostic(response);
24
+ const diagnostics = [...built.diagnostics];
25
+ if (!responseDiagnostic.ok) {
26
+ diagnostics.push(createDiagnostic(input, 'http-error', `Endpoint test request returned HTTP status ${responseDiagnostic.status}.`));
27
+ }
28
+ return responseDiagnostic.ok
29
+ ? {
30
+ ok: true,
31
+ request: built.request,
32
+ response: responseDiagnostic,
33
+ data: responseDiagnostic.parsedBody,
34
+ diagnostics,
35
+ }
36
+ : {
37
+ ok: false,
38
+ request: built.request,
39
+ response: responseDiagnostic,
40
+ diagnostics,
41
+ };
42
+ }
43
+ catch (error) {
44
+ return {
45
+ ok: false,
46
+ request: built.request,
47
+ diagnostics: [
48
+ ...built.diagnostics,
49
+ createDiagnostic(input, 'network-error', createNetworkErrorMessage(error)),
50
+ ],
51
+ };
52
+ }
53
+ }
54
+ export async function buildEndpointTestRequest(input) {
55
+ const selection = selectEndpointOperation(input);
56
+ if (selection === undefined) {
57
+ return {
58
+ ok: false,
59
+ diagnostics: [
60
+ createDiagnostic(input, 'missing-operation', 'Endpoint or operation could not be found.'),
61
+ ],
62
+ };
63
+ }
64
+ const diagnostics = [];
65
+ const credential = await resolveEndpointCredential(input, selection, diagnostics);
66
+ const request = selection.operation.protocol === 'graphql'
67
+ ? buildGraphQlRequest(input, selection, credential, diagnostics)
68
+ : buildHttpRequest(input, selection, credential, diagnostics);
69
+ if (request === undefined || diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
70
+ return {
71
+ ok: false,
72
+ request,
73
+ diagnostics,
74
+ };
75
+ }
76
+ return {
77
+ ok: true,
78
+ request,
79
+ diagnostics,
80
+ };
81
+ }
82
+ function selectEndpointOperation(input) {
83
+ const endpoint = input.dataSource.endpoints[input.endpointId];
84
+ const operation = endpoint?.operations[input.operationId];
85
+ if (endpoint === undefined || operation === undefined)
86
+ return undefined;
87
+ return { endpoint, operation };
88
+ }
89
+ async function resolveEndpointCredential(input, selection, diagnostics) {
90
+ const credentialRef = selection.operation.credential ?? selection.endpoint.credential ?? input.dataSource.credential;
91
+ if (credentialRef === undefined)
92
+ return undefined;
93
+ if (input.credentialResolver === undefined) {
94
+ diagnostics.push(createDiagnostic(input, 'missing-credential', `Credential '${credentialRef.id}' requires a resolver.`));
95
+ return undefined;
96
+ }
97
+ const credential = await input.credentialResolver(credentialRef);
98
+ if (credential === undefined) {
99
+ diagnostics.push(createDiagnostic(input, 'missing-credential', `Credential '${credentialRef.id}' could not be resolved.`));
100
+ }
101
+ return credential;
102
+ }
103
+ function buildHttpRequest(input, selection, credential, diagnostics) {
104
+ const baseUrl = getDataSourceBaseUrl(input.dataSource, selection.endpoint);
105
+ const path = selection.operation.path ?? selection.endpoint.path;
106
+ if (baseUrl === undefined || baseUrl.trim().length === 0) {
107
+ diagnostics.push(createDiagnostic(input, 'invalid-config', 'HTTP endpoint requires a base URL.'));
108
+ return undefined;
109
+ }
110
+ if (path === undefined) {
111
+ diagnostics.push(createDiagnostic(input, 'invalid-config', 'HTTP operation requires a path.'));
112
+ return undefined;
113
+ }
114
+ const values = input.values ?? {};
115
+ const parameters = selection.operation.request?.parameters ?? [];
116
+ const pathResult = interpolatePath(path, parameters, values, input, diagnostics);
117
+ const query = collectQuery(parameters, values, credential?.query);
118
+ const url = appendQuery(joinUrl(baseUrl, pathResult), query);
119
+ const body = createHttpBody(selection.operation, values);
120
+ const headers = {
121
+ ...(credential?.headers ?? {}),
122
+ };
123
+ if (body !== undefined) {
124
+ headers['content-type'] = headers['content-type'] ?? 'application/json';
125
+ }
126
+ return {
127
+ dataSourceId: input.dataSource.id,
128
+ endpointId: input.endpointId,
129
+ operationId: input.operationId,
130
+ url,
131
+ method: selection.operation.method ?? 'GET',
132
+ headers,
133
+ body,
134
+ dryRun: input.dryRun === true,
135
+ };
136
+ }
137
+ function buildGraphQlRequest(input, selection, credential, diagnostics) {
138
+ const endpointUrl = getGraphQlEndpointUrl(input.dataSource, selection.endpoint);
139
+ if (endpointUrl === undefined || endpointUrl.trim().length === 0) {
140
+ diagnostics.push(createDiagnostic(input, 'invalid-config', 'GraphQL endpoint requires an endpoint URL.'));
141
+ return undefined;
142
+ }
143
+ const query = getStringMetadataValue(selection.operation.metadata, 'document') ??
144
+ getStringValue(input.values, 'query');
145
+ if (query === undefined) {
146
+ diagnostics.push(createDiagnostic(input, 'invalid-config', 'GraphQL operation requires a query document.'));
147
+ return undefined;
148
+ }
149
+ const variables = getRecordValue(input.values, 'variables');
150
+ const body = JSON.stringify({ query, variables: variables ?? {} });
151
+ const headers = {
152
+ ...(credential?.headers ?? {}),
153
+ 'content-type': 'application/json',
154
+ };
155
+ return {
156
+ dataSourceId: input.dataSource.id,
157
+ endpointId: input.endpointId,
158
+ operationId: input.operationId,
159
+ url: appendQuery(endpointUrl, credential?.query ?? {}),
160
+ method: 'POST',
161
+ headers,
162
+ body,
163
+ dryRun: input.dryRun === true,
164
+ };
165
+ }
166
+ function getDataSourceBaseUrl(dataSource, endpoint) {
167
+ if (endpoint.baseUrl !== undefined)
168
+ return endpoint.baseUrl;
169
+ if (dataSource.kind === 'rest' || dataSource.kind === 'openapi')
170
+ return dataSource.baseUrl;
171
+ if (dataSource.kind === 'graphql')
172
+ return dataSource.endpointUrl;
173
+ return undefined;
174
+ }
175
+ function getGraphQlEndpointUrl(dataSource, endpoint) {
176
+ if (endpoint.baseUrl !== undefined)
177
+ return endpoint.baseUrl;
178
+ if (dataSource.kind === 'graphql')
179
+ return dataSource.endpointUrl;
180
+ return undefined;
181
+ }
182
+ function interpolatePath(path, parameters, values, input, diagnostics) {
183
+ let nextPath = path;
184
+ const pathParameters = parameters.filter((parameter) => parameter.location === 'path');
185
+ for (const parameter of pathParameters) {
186
+ const value = values[parameter.name] ?? parameter.default;
187
+ if (value === undefined) {
188
+ diagnostics.push(createDiagnostic(input, 'invalid-config', `Missing required path parameter '${parameter.name}'.`));
189
+ continue;
190
+ }
191
+ const encodedValue = encodeURIComponent(serializeEndpointUrlValue(value));
192
+ nextPath = nextPath.replaceAll(`{${parameter.name}}`, encodedValue);
193
+ nextPath = nextPath.replaceAll(`:${parameter.name}`, encodedValue);
194
+ }
195
+ return nextPath;
196
+ }
197
+ function collectQuery(parameters, values, credentialQuery) {
198
+ const query = { ...(credentialQuery ?? {}) };
199
+ for (const parameter of parameters) {
200
+ if (parameter.location !== 'query')
201
+ continue;
202
+ const value = values[parameter.name] ?? parameter.default;
203
+ if (value !== undefined)
204
+ query[parameter.name] = value;
205
+ }
206
+ return query;
207
+ }
208
+ function createHttpBody(operation, values) {
209
+ if (operation.method === 'GET' || operation.method === 'HEAD')
210
+ return undefined;
211
+ const { body } = values;
212
+ if (body === undefined)
213
+ return undefined;
214
+ return JSON.stringify(body);
215
+ }
216
+ async function createResponseDiagnostic(response) {
217
+ const bodyText = await response.text();
218
+ const parsedBody = parseResponseBody(bodyText);
219
+ return {
220
+ status: response.status,
221
+ ok: response.status >= 200 && response.status < 300,
222
+ headers: response.headers,
223
+ bodyText,
224
+ parsedBody,
225
+ };
226
+ }
227
+ function parseResponseBody(bodyText) {
228
+ if (bodyText.trim().length === 0)
229
+ return undefined;
230
+ try {
231
+ const parsed = JSON.parse(bodyText);
232
+ return isDataContractValue(parsed) ? parsed : { raw: bodyText };
233
+ }
234
+ catch {
235
+ return { raw: bodyText };
236
+ }
237
+ }
238
+ function appendQuery(url, query) {
239
+ const entries = Object.entries(query);
240
+ if (entries.length === 0)
241
+ return url;
242
+ const search = entries
243
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(serializeEndpointUrlValue(value))}`)
244
+ .join('&');
245
+ const separator = url.includes('?') ? '&' : '?';
246
+ return `${url}${separator}${search}`;
247
+ }
248
+ function serializeEndpointUrlValue(value) {
249
+ if (typeof value === 'string')
250
+ return value;
251
+ if (typeof value === 'number' || typeof value === 'boolean')
252
+ return value.toString();
253
+ if (value === null)
254
+ return '';
255
+ return JSON.stringify(value);
256
+ }
257
+ function joinUrl(baseUrl, path) {
258
+ const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
259
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
260
+ return `${normalizedBase}${normalizedPath}`;
261
+ }
262
+ function getStringValue(values, key) {
263
+ const value = values?.[key];
264
+ return typeof value === 'string' ? value : undefined;
265
+ }
266
+ function getRecordValue(values, key) {
267
+ const value = values?.[key];
268
+ return isDataContractValue(value) ? value : undefined;
269
+ }
270
+ function getStringMetadataValue(metadata, key) {
271
+ if (!isDataContractRecord(metadata))
272
+ return undefined;
273
+ const value = metadata[key];
274
+ return typeof value === 'string' ? value : undefined;
275
+ }
276
+ function createDiagnostic(input, code, message) {
277
+ return {
278
+ code,
279
+ dataSourceId: input.dataSource.id,
280
+ endpointId: input.endpointId,
281
+ operationId: input.operationId,
282
+ message,
283
+ severity: code === 'missing-schema' ? 'info' : 'error',
284
+ };
285
+ }
286
+ function createNetworkErrorMessage(error) {
287
+ if (error instanceof Error)
288
+ return error.message;
289
+ return 'Endpoint test request failed.';
290
+ }
291
+ function isDataContractValue(value) {
292
+ if (value === null ||
293
+ typeof value === 'string' ||
294
+ typeof value === 'number' ||
295
+ typeof value === 'boolean') {
296
+ return true;
297
+ }
298
+ if (Array.isArray(value))
299
+ return value.every(isDataContractValue);
300
+ if (typeof value !== 'object')
301
+ return false;
302
+ return Object.values(value).every(isDataContractValue);
303
+ }
304
+ function isDataContractRecord(value) {
305
+ return (value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value));
306
+ }
307
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/test-runner/index.ts"],"names":[],"mappings":"AA0FA,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAwB;IACzD,MAAM,KAAK,GAAG,MAAM,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAEpD,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IAC5B,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAExC,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,WAAW,EAAE;gBACX,GAAG,KAAK,CAAC,WAAW;gBACpB,gBAAgB,CACd,KAAK,EACL,gBAAgB,EAChB,2DAA2D,CAC5D;aACF;SACF,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;YACpD,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM;YAC5B,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO;YAC9B,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI;SACzB,CAAC,CAAC;QACH,MAAM,kBAAkB,GAAG,MAAM,wBAAwB,CAAC,QAAQ,CAAC,CAAC;QACpE,MAAM,WAAW,GAAG,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;QAE3C,IAAI,CAAC,kBAAkB,CAAC,EAAE,EAAE,CAAC;YAC3B,WAAW,CAAC,IAAI,CACd,gBAAgB,CACd,KAAK,EACL,YAAY,EACZ,8CAA8C,kBAAkB,CAAC,MAAM,GAAG,CAC3E,CACF,CAAC;QACJ,CAAC;QAED,OAAO,kBAAkB,CAAC,EAAE;YAC1B,CAAC,CAAC;gBACE,EAAE,EAAE,IAAI;gBACR,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,QAAQ,EAAE,kBAAkB;gBAC5B,IAAI,EAAE,kBAAkB,CAAC,UAAU;gBACnC,WAAW;aACZ;YACH,CAAC,CAAC;gBACE,EAAE,EAAE,KAAK;gBACT,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,QAAQ,EAAE,kBAAkB;gBAC5B,WAAW;aACZ,CAAC;IACR,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,WAAW,EAAE;gBACX,GAAG,KAAK,CAAC,WAAW;gBACpB,gBAAgB,CAAC,KAAK,EAAE,eAAe,EAAE,yBAAyB,CAAC,KAAK,CAAC,CAAC;aAC3E;SACF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,KAAwB;IAExB,MAAM,SAAS,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAC;IACjD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,WAAW,EAAE;gBACX,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,EAAE,2CAA2C,CAAC;aAC1F;SACF,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAA2B,EAAE,CAAC;IAC/C,MAAM,UAAU,GAAG,MAAM,yBAAyB,CAAC,KAAK,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;IAClF,MAAM,OAAO,GACX,SAAS,CAAC,SAAS,CAAC,QAAQ,KAAK,SAAS;QACxC,CAAC,CAAC,mBAAmB,CAAC,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC;QAChE,CAAC,CAAC,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;IAElE,IAAI,OAAO,KAAK,SAAS,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;QAC/F,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO;YACP,WAAW;SACZ,CAAC;IACJ,CAAC;IAED,OAAO;QACL,EAAE,EAAE,IAAI;QACR,OAAO;QACP,WAAW;KACZ,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAAC,KAAwB;IACvD,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC9D,MAAM,SAAS,GAAG,QAAQ,EAAE,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAE1D,IAAI,QAAQ,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAExE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACjC,CAAC;AAED,KAAK,UAAU,yBAAyB,CACtC,KAAwB,EACxB,SAAqC,EACrC,WAAmC;IAEnC,MAAM,aAAa,GACjB,SAAS,CAAC,SAAS,CAAC,UAAU,IAAI,SAAS,CAAC,QAAQ,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC;IAEjG,IAAI,aAAa,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAElD,IAAI,KAAK,CAAC,kBAAkB,KAAK,SAAS,EAAE,CAAC;QAC3C,WAAW,CAAC,IAAI,CACd,gBAAgB,CACd,KAAK,EACL,oBAAoB,EACpB,eAAe,aAAa,CAAC,EAAE,wBAAwB,CACxD,CACF,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,KAAK,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;IAEjE,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,WAAW,CAAC,IAAI,CACd,gBAAgB,CACd,KAAK,EACL,oBAAoB,EACpB,eAAe,aAAa,CAAC,EAAE,0BAA0B,CAC1D,CACF,CAAC;IACJ,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,gBAAgB,CACvB,KAAwB,EACxB,SAAqC,EACrC,UAA8C,EAC9C,WAAmC;IAEnC,MAAM,OAAO,GAAG,oBAAoB,CAAC,KAAK,CAAC,UAAU,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC3E,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;IAEjE,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzD,WAAW,CAAC,IAAI,CACd,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,oCAAoC,CAAC,CAChF,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,WAAW,CAAC,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,iCAAiC,CAAC,CAAC,CAAC;QAC/F,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC;IAClC,MAAM,UAAU,GAAG,SAAS,CAAC,SAAS,CAAC,OAAO,EAAE,UAAU,IAAI,EAAE,CAAC;IACjE,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;IACjF,MAAM,KAAK,GAAG,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;IAClE,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,CAAC;IAC7D,MAAM,IAAI,GAAG,cAAc,CAAC,SAAS,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACzD,MAAM,OAAO,GAA2B;QACtC,GAAG,CAAC,UAAU,EAAE,OAAO,IAAI,EAAE,CAAC;KAC/B,CAAC;IAEF,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,IAAI,kBAAkB,CAAC;IAC1E,CAAC;IAED,OAAO;QACL,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE;QACjC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,GAAG;QACH,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,IAAI,KAAK;QAC3C,OAAO;QACP,IAAI;QACJ,MAAM,EAAE,KAAK,CAAC,MAAM,KAAK,IAAI;KAC9B,CAAC;AACJ,CAAC;AAED,SAAS,mBAAmB,CAC1B,KAAwB,EACxB,SAAqC,EACrC,UAA8C,EAC9C,WAAmC;IAEnC,MAAM,WAAW,GAAG,qBAAqB,CAAC,KAAK,CAAC,UAAU,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAEhF,IAAI,WAAW,KAAK,SAAS,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjE,WAAW,CAAC,IAAI,CACd,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,4CAA4C,CAAC,CACxF,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,KAAK,GACT,sBAAsB,CAAC,SAAS,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC;QAChE,cAAc,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAExC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,WAAW,CAAC,IAAI,CACd,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,8CAA8C,CAAC,CAC1F,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,SAAS,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,IAAI,EAAE,EAAE,CAAC,CAAC;IACnE,MAAM,OAAO,GAA2B;QACtC,GAAG,CAAC,UAAU,EAAE,OAAO,IAAI,EAAE,CAAC;QAC9B,cAAc,EAAE,kBAAkB;KACnC,CAAC;IAEF,OAAO;QACL,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE;QACjC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,GAAG,EAAE,WAAW,CAAC,WAAW,EAAE,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC;QACtD,MAAM,EAAE,MAAM;QACd,OAAO;QACP,IAAI;QACJ,MAAM,EAAE,KAAK,CAAC,MAAM,KAAK,IAAI;KAC9B,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAC3B,UAA4B,EAC5B,QAA4B;IAE5B,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC5D,IAAI,UAAU,CAAC,IAAI,KAAK,MAAM,IAAI,UAAU,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,OAAO,CAAC;IAC3F,IAAI,UAAU,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,WAAW,CAAC;IACjE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,qBAAqB,CAC5B,UAA4B,EAC5B,QAA4B;IAE5B,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC5D,IAAI,UAAU,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,WAAW,CAAC;IACjE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,eAAe,CACtB,IAAY,EACZ,UAA6C,EAC7C,MAA+B,EAC/B,KAAwB,EACxB,WAAmC;IAEnC,IAAI,QAAQ,GAAG,IAAI,CAAC;IACpB,MAAM,cAAc,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC;IAEvF,KAAK,MAAM,SAAS,IAAI,cAAc,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,OAAO,CAAC;QAE1D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,WAAW,CAAC,IAAI,CACd,gBAAgB,CACd,KAAK,EACL,gBAAgB,EAChB,oCAAoC,SAAS,CAAC,IAAI,IAAI,CACvD,CACF,CAAC;YACF,SAAS;QACX,CAAC;QAED,MAAM,YAAY,GAAG,kBAAkB,CAAC,yBAAyB,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1E,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,SAAS,CAAC,IAAI,GAAG,EAAE,YAAY,CAAC,CAAC;QACpE,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,YAAY,CAAC,CAAC;IACrE,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,YAAY,CACnB,UAA6C,EAC7C,MAA+B,EAC/B,eAAoD;IAEpD,MAAM,KAAK,GAAsC,EAAE,GAAG,CAAC,eAAe,IAAI,EAAE,CAAC,EAAE,CAAC;IAEhF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,SAAS,CAAC,QAAQ,KAAK,OAAO;YAAE,SAAS;QAE7C,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,OAAO,CAAC;QAC1D,IAAI,KAAK,KAAK,SAAS;YAAE,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACzD,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,cAAc,CACrB,SAA8B,EAC9B,MAA+B;IAE/B,IAAI,SAAS,CAAC,MAAM,KAAK,KAAK,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,SAAS,CAAC;IAEhF,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;IACxB,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAEzC,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,KAAK,UAAU,wBAAwB,CACrC,QAAmC;IAEnC,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACvC,MAAM,UAAU,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAE/C,OAAO;QACL,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,EAAE,EAAE,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG;QACnD,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,QAAQ;QACR,UAAU;KACX,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAgB;IACzC,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAEnD,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7C,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;IAC3B,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,GAAW,EAAE,KAA8B;IAC9D,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAErC,MAAM,MAAM,GAAG,OAAO;SACnB,GAAG,CACF,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CACf,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,yBAAyB,CAAC,KAAK,CAAC,CAAC,EAAE,CACvF;SACA,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,MAAM,SAAS,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IAEhD,OAAO,GAAG,GAAG,GAAG,SAAS,GAAG,MAAM,EAAE,CAAC;AACvC,CAAC;AAED,SAAS,yBAAyB,CAAC,KAAwB;IACzD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;IACrF,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAE9B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,OAAO,CAAC,OAAe,EAAE,IAAY;IAC5C,MAAM,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC9E,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IAEhE,OAAO,GAAG,cAAc,GAAG,cAAc,EAAE,CAAC;AAC9C,CAAC;AAED,SAAS,cAAc,CACrB,MAA2C,EAC3C,GAAW;IAEX,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;IAC5B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvD,CAAC;AAED,SAAS,cAAc,CACrB,MAA2C,EAC3C,GAAW;IAEX,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;IAC5B,OAAO,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACxD,CAAC;AAED,SAAS,sBAAsB,CAC7B,QAAuC,EACvC,GAAW;IAEX,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC;IAEtD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvD,CAAC;AAED,SAAS,gBAAgB,CACvB,KAAwB,EACxB,IAAY,EACZ,OAAe;IAEf,OAAO;QACL,IAAI;QACJ,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE;QACjC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,OAAO;QACP,QAAQ,EAAE,IAAI,KAAK,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;KACvD,CAAC;AACJ,CAAC;AAED,SAAS,yBAAyB,CAAC,KAAc;IAC/C,IAAI,KAAK,YAAY,KAAK;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC;IACjD,OAAO,+BAA+B,CAAC;AACzC,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAc;IACzC,IACE,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,OAAO,KAAK,KAAK,QAAQ;QACzB,OAAO,KAAK,KAAK,SAAS,EAC1B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;IAClE,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE5C,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,oBAAoB,CAC3B,KAAoC;IAEpC,OAAO,CACL,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAC5F,CAAC;AACJ,CAAC","sourcesContent":["import type {\n CredentialRef,\n DataContractValue,\n DataEndpointConfig,\n DataOperationConfig,\n DataOperationParameter,\n DataSourceConfig,\n DataSourceDiagnostic,\n EndpointId,\n OperationId,\n} from '@ankhorage/contracts/data';\n\nexport type EndpointTestInputValues = Readonly<Record<string, DataContractValue>>;\nexport type EndpointTestHeaders = Readonly<Record<string, string>>;\n\nexport interface EndpointTestCredential {\n readonly headers?: EndpointTestHeaders;\n readonly query?: EndpointTestInputValues;\n}\n\nexport type EndpointTestCredentialResolver = (\n credential: CredentialRef,\n) => EndpointTestCredential | Promise<EndpointTestCredential | undefined> | undefined;\n\nexport interface EndpointTestFetchInit {\n readonly method: string;\n readonly headers: EndpointTestHeaders;\n readonly body?: string;\n}\n\nexport interface EndpointTestFetchResponse {\n readonly status: number;\n readonly headers?: EndpointTestHeaders;\n text(): Promise<string>;\n}\n\nexport type EndpointTestFetch = (\n url: string,\n init: EndpointTestFetchInit,\n) => Promise<EndpointTestFetchResponse>;\n\nexport interface EndpointTestInput {\n readonly dataSource: DataSourceConfig;\n readonly endpointId: EndpointId;\n readonly operationId: OperationId;\n readonly values?: EndpointTestInputValues;\n readonly dryRun?: boolean;\n readonly fetch?: EndpointTestFetch;\n readonly credentialResolver?: EndpointTestCredentialResolver;\n}\n\nexport interface EndpointTestRequestDiagnostic {\n readonly dataSourceId: string;\n readonly endpointId: EndpointId;\n readonly operationId: OperationId;\n readonly url: string;\n readonly method: string;\n readonly headers: EndpointTestHeaders;\n readonly body?: string;\n readonly dryRun: boolean;\n}\n\nexport interface EndpointTestResponseDiagnostic {\n readonly status: number;\n readonly ok: boolean;\n readonly headers?: EndpointTestHeaders;\n readonly bodyText?: string;\n readonly parsedBody?: DataContractValue;\n}\n\nexport type EndpointTestResult =\n | {\n readonly ok: true;\n readonly request: EndpointTestRequestDiagnostic;\n readonly response?: EndpointTestResponseDiagnostic;\n readonly data?: DataContractValue;\n readonly diagnostics: readonly DataSourceDiagnostic[];\n }\n | {\n readonly ok: false;\n readonly request?: EndpointTestRequestDiagnostic;\n readonly response?: EndpointTestResponseDiagnostic;\n readonly diagnostics: readonly DataSourceDiagnostic[];\n };\n\ninterface EndpointOperationSelection {\n readonly endpoint: DataEndpointConfig;\n readonly operation: DataOperationConfig;\n}\n\nexport async function testEndpoint(input: EndpointTestInput): Promise<EndpointTestResult> {\n const built = await buildEndpointTestRequest(input);\n\n if (!built.ok) return built;\n if (input.dryRun === true) return built;\n\n if (input.fetch === undefined) {\n return {\n ok: false,\n request: built.request,\n diagnostics: [\n ...built.diagnostics,\n createDiagnostic(\n input,\n 'invalid-config',\n 'Endpoint test runner requires an injected fetch function.',\n ),\n ],\n };\n }\n\n try {\n const response = await input.fetch(built.request.url, {\n method: built.request.method,\n headers: built.request.headers,\n body: built.request.body,\n });\n const responseDiagnostic = await createResponseDiagnostic(response);\n const diagnostics = [...built.diagnostics];\n\n if (!responseDiagnostic.ok) {\n diagnostics.push(\n createDiagnostic(\n input,\n 'http-error',\n `Endpoint test request returned HTTP status ${responseDiagnostic.status}.`,\n ),\n );\n }\n\n return responseDiagnostic.ok\n ? {\n ok: true,\n request: built.request,\n response: responseDiagnostic,\n data: responseDiagnostic.parsedBody,\n diagnostics,\n }\n : {\n ok: false,\n request: built.request,\n response: responseDiagnostic,\n diagnostics,\n };\n } catch (error) {\n return {\n ok: false,\n request: built.request,\n diagnostics: [\n ...built.diagnostics,\n createDiagnostic(input, 'network-error', createNetworkErrorMessage(error)),\n ],\n };\n }\n}\n\nexport async function buildEndpointTestRequest(\n input: EndpointTestInput,\n): Promise<EndpointTestResult> {\n const selection = selectEndpointOperation(input);\n if (selection === undefined) {\n return {\n ok: false,\n diagnostics: [\n createDiagnostic(input, 'missing-operation', 'Endpoint or operation could not be found.'),\n ],\n };\n }\n\n const diagnostics: DataSourceDiagnostic[] = [];\n const credential = await resolveEndpointCredential(input, selection, diagnostics);\n const request =\n selection.operation.protocol === 'graphql'\n ? buildGraphQlRequest(input, selection, credential, diagnostics)\n : buildHttpRequest(input, selection, credential, diagnostics);\n\n if (request === undefined || diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {\n return {\n ok: false,\n request,\n diagnostics,\n };\n }\n\n return {\n ok: true,\n request,\n diagnostics,\n };\n}\n\nfunction selectEndpointOperation(input: EndpointTestInput): EndpointOperationSelection | undefined {\n const endpoint = input.dataSource.endpoints[input.endpointId];\n const operation = endpoint?.operations[input.operationId];\n\n if (endpoint === undefined || operation === undefined) return undefined;\n\n return { endpoint, operation };\n}\n\nasync function resolveEndpointCredential(\n input: EndpointTestInput,\n selection: EndpointOperationSelection,\n diagnostics: DataSourceDiagnostic[],\n): Promise<EndpointTestCredential | undefined> {\n const credentialRef =\n selection.operation.credential ?? selection.endpoint.credential ?? input.dataSource.credential;\n\n if (credentialRef === undefined) return undefined;\n\n if (input.credentialResolver === undefined) {\n diagnostics.push(\n createDiagnostic(\n input,\n 'missing-credential',\n `Credential '${credentialRef.id}' requires a resolver.`,\n ),\n );\n return undefined;\n }\n\n const credential = await input.credentialResolver(credentialRef);\n\n if (credential === undefined) {\n diagnostics.push(\n createDiagnostic(\n input,\n 'missing-credential',\n `Credential '${credentialRef.id}' could not be resolved.`,\n ),\n );\n }\n\n return credential;\n}\n\nfunction buildHttpRequest(\n input: EndpointTestInput,\n selection: EndpointOperationSelection,\n credential: EndpointTestCredential | undefined,\n diagnostics: DataSourceDiagnostic[],\n): EndpointTestRequestDiagnostic | undefined {\n const baseUrl = getDataSourceBaseUrl(input.dataSource, selection.endpoint);\n const path = selection.operation.path ?? selection.endpoint.path;\n\n if (baseUrl === undefined || baseUrl.trim().length === 0) {\n diagnostics.push(\n createDiagnostic(input, 'invalid-config', 'HTTP endpoint requires a base URL.'),\n );\n return undefined;\n }\n\n if (path === undefined) {\n diagnostics.push(createDiagnostic(input, 'invalid-config', 'HTTP operation requires a path.'));\n return undefined;\n }\n\n const values = input.values ?? {};\n const parameters = selection.operation.request?.parameters ?? [];\n const pathResult = interpolatePath(path, parameters, values, input, diagnostics);\n const query = collectQuery(parameters, values, credential?.query);\n const url = appendQuery(joinUrl(baseUrl, pathResult), query);\n const body = createHttpBody(selection.operation, values);\n const headers: Record<string, string> = {\n ...(credential?.headers ?? {}),\n };\n\n if (body !== undefined) {\n headers['content-type'] = headers['content-type'] ?? 'application/json';\n }\n\n return {\n dataSourceId: input.dataSource.id,\n endpointId: input.endpointId,\n operationId: input.operationId,\n url,\n method: selection.operation.method ?? 'GET',\n headers,\n body,\n dryRun: input.dryRun === true,\n };\n}\n\nfunction buildGraphQlRequest(\n input: EndpointTestInput,\n selection: EndpointOperationSelection,\n credential: EndpointTestCredential | undefined,\n diagnostics: DataSourceDiagnostic[],\n): EndpointTestRequestDiagnostic | undefined {\n const endpointUrl = getGraphQlEndpointUrl(input.dataSource, selection.endpoint);\n\n if (endpointUrl === undefined || endpointUrl.trim().length === 0) {\n diagnostics.push(\n createDiagnostic(input, 'invalid-config', 'GraphQL endpoint requires an endpoint URL.'),\n );\n return undefined;\n }\n\n const query =\n getStringMetadataValue(selection.operation.metadata, 'document') ??\n getStringValue(input.values, 'query');\n\n if (query === undefined) {\n diagnostics.push(\n createDiagnostic(input, 'invalid-config', 'GraphQL operation requires a query document.'),\n );\n return undefined;\n }\n\n const variables = getRecordValue(input.values, 'variables');\n const body = JSON.stringify({ query, variables: variables ?? {} });\n const headers: Record<string, string> = {\n ...(credential?.headers ?? {}),\n 'content-type': 'application/json',\n };\n\n return {\n dataSourceId: input.dataSource.id,\n endpointId: input.endpointId,\n operationId: input.operationId,\n url: appendQuery(endpointUrl, credential?.query ?? {}),\n method: 'POST',\n headers,\n body,\n dryRun: input.dryRun === true,\n };\n}\n\nfunction getDataSourceBaseUrl(\n dataSource: DataSourceConfig,\n endpoint: DataEndpointConfig,\n): string | undefined {\n if (endpoint.baseUrl !== undefined) return endpoint.baseUrl;\n if (dataSource.kind === 'rest' || dataSource.kind === 'openapi') return dataSource.baseUrl;\n if (dataSource.kind === 'graphql') return dataSource.endpointUrl;\n return undefined;\n}\n\nfunction getGraphQlEndpointUrl(\n dataSource: DataSourceConfig,\n endpoint: DataEndpointConfig,\n): string | undefined {\n if (endpoint.baseUrl !== undefined) return endpoint.baseUrl;\n if (dataSource.kind === 'graphql') return dataSource.endpointUrl;\n return undefined;\n}\n\nfunction interpolatePath(\n path: string,\n parameters: readonly DataOperationParameter[],\n values: EndpointTestInputValues,\n input: EndpointTestInput,\n diagnostics: DataSourceDiagnostic[],\n): string {\n let nextPath = path;\n const pathParameters = parameters.filter((parameter) => parameter.location === 'path');\n\n for (const parameter of pathParameters) {\n const value = values[parameter.name] ?? parameter.default;\n\n if (value === undefined) {\n diagnostics.push(\n createDiagnostic(\n input,\n 'invalid-config',\n `Missing required path parameter '${parameter.name}'.`,\n ),\n );\n continue;\n }\n\n const encodedValue = encodeURIComponent(serializeEndpointUrlValue(value));\n nextPath = nextPath.replaceAll(`{${parameter.name}}`, encodedValue);\n nextPath = nextPath.replaceAll(`:${parameter.name}`, encodedValue);\n }\n\n return nextPath;\n}\n\nfunction collectQuery(\n parameters: readonly DataOperationParameter[],\n values: EndpointTestInputValues,\n credentialQuery: EndpointTestInputValues | undefined,\n): EndpointTestInputValues {\n const query: Record<string, DataContractValue> = { ...(credentialQuery ?? {}) };\n\n for (const parameter of parameters) {\n if (parameter.location !== 'query') continue;\n\n const value = values[parameter.name] ?? parameter.default;\n if (value !== undefined) query[parameter.name] = value;\n }\n\n return query;\n}\n\nfunction createHttpBody(\n operation: DataOperationConfig,\n values: EndpointTestInputValues,\n): string | undefined {\n if (operation.method === 'GET' || operation.method === 'HEAD') return undefined;\n\n const { body } = values;\n if (body === undefined) return undefined;\n\n return JSON.stringify(body);\n}\n\nasync function createResponseDiagnostic(\n response: EndpointTestFetchResponse,\n): Promise<EndpointTestResponseDiagnostic> {\n const bodyText = await response.text();\n const parsedBody = parseResponseBody(bodyText);\n\n return {\n status: response.status,\n ok: response.status >= 200 && response.status < 300,\n headers: response.headers,\n bodyText,\n parsedBody,\n };\n}\n\nfunction parseResponseBody(bodyText: string): DataContractValue | undefined {\n if (bodyText.trim().length === 0) return undefined;\n\n try {\n const parsed: unknown = JSON.parse(bodyText);\n return isDataContractValue(parsed) ? parsed : { raw: bodyText };\n } catch {\n return { raw: bodyText };\n }\n}\n\nfunction appendQuery(url: string, query: EndpointTestInputValues): string {\n const entries = Object.entries(query);\n if (entries.length === 0) return url;\n\n const search = entries\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key)}=${encodeURIComponent(serializeEndpointUrlValue(value))}`,\n )\n .join('&');\n const separator = url.includes('?') ? '&' : '?';\n\n return `${url}${separator}${search}`;\n}\n\nfunction serializeEndpointUrlValue(value: DataContractValue): string {\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean') return value.toString();\n if (value === null) return '';\n\n return JSON.stringify(value);\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n\n return `${normalizedBase}${normalizedPath}`;\n}\n\nfunction getStringValue(\n values: EndpointTestInputValues | undefined,\n key: string,\n): string | undefined {\n const value = values?.[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction getRecordValue(\n values: EndpointTestInputValues | undefined,\n key: string,\n): DataContractValue | undefined {\n const value = values?.[key];\n return isDataContractValue(value) ? value : undefined;\n}\n\nfunction getStringMetadataValue(\n metadata: DataContractValue | undefined,\n key: string,\n): string | undefined {\n if (!isDataContractRecord(metadata)) return undefined;\n\n const value = metadata[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction createDiagnostic(\n input: EndpointTestInput,\n code: string,\n message: string,\n): DataSourceDiagnostic {\n return {\n code,\n dataSourceId: input.dataSource.id,\n endpointId: input.endpointId,\n operationId: input.operationId,\n message,\n severity: code === 'missing-schema' ? 'info' : 'error',\n };\n}\n\nfunction createNetworkErrorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n return 'Endpoint test request failed.';\n}\n\nfunction isDataContractValue(value: unknown): value is DataContractValue {\n if (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return true;\n }\n\n if (Array.isArray(value)) return value.every(isDataContractValue);\n if (typeof value !== 'object') return false;\n\n return Object.values(value).every(isDataContractValue);\n}\n\nfunction isDataContractRecord(\n value: DataContractValue | undefined,\n): value is Record<string, DataContractValue> {\n return (\n value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value)\n );\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/data-sources",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "description": "Headless toolkit for importing, generating, normalizing, and testing Ankhorage data-source configs.",
6
6
  "homepage": "https://github.com/ankhorage/data-sources#readme",
7
7
  "bugs": {
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from './graphql';
4
4
  export * from './managed-api';
5
5
  export * from './openapi';
6
6
  export * from './rest';
7
+ export * from './test-runner';
7
8
 
8
9
  export const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;
9
10
 
@@ -0,0 +1,266 @@
1
+ import type { DataSourceConfig } from '@ankhorage/contracts/data';
2
+ import { describe, expect, it } from 'bun:test';
3
+
4
+ import { buildEndpointTestRequest, type EndpointTestFetch, testEndpoint } from './index';
5
+
6
+ function createRestDataSource(): DataSourceConfig {
7
+ return {
8
+ id: 'cms',
9
+ kind: 'rest',
10
+ baseUrl: 'https://cms.example.com',
11
+ credential: { id: 'cms-token', kind: 'bearer' },
12
+ endpoints: {
13
+ posts: {
14
+ id: 'posts',
15
+ kind: 'http',
16
+ path: '/posts/{postId}',
17
+ operations: {
18
+ 'posts.get': {
19
+ id: 'posts.get',
20
+ endpointId: 'posts',
21
+ protocol: 'http',
22
+ intent: 'read',
23
+ method: 'GET',
24
+ path: '/posts/{postId}',
25
+ request: {
26
+ parameters: [
27
+ {
28
+ name: 'postId',
29
+ location: 'path',
30
+ required: true,
31
+ schema: { type: 'string' },
32
+ },
33
+ {
34
+ name: 'preview',
35
+ location: 'query',
36
+ schema: { type: 'boolean' },
37
+ },
38
+ ],
39
+ },
40
+ response: {
41
+ status: 200,
42
+ schema: {
43
+ type: 'object',
44
+ },
45
+ },
46
+ },
47
+ 'posts.create': {
48
+ id: 'posts.create',
49
+ endpointId: 'posts',
50
+ protocol: 'http',
51
+ intent: 'create',
52
+ method: 'POST',
53
+ path: '/posts',
54
+ request: {
55
+ contentType: 'application/json',
56
+ schema: { type: 'object' },
57
+ },
58
+ response: {
59
+ status: 201,
60
+ schema: { type: 'object' },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ },
66
+ };
67
+ }
68
+
69
+ function createGraphQlDataSource(): DataSourceConfig {
70
+ return {
71
+ id: 'content',
72
+ kind: 'graphql',
73
+ endpointUrl: 'https://content.example.com/graphql',
74
+ endpoints: {
75
+ graphql: {
76
+ id: 'graphql',
77
+ kind: 'graphql',
78
+ operations: {
79
+ 'query.viewer': {
80
+ id: 'query.viewer',
81
+ endpointId: 'graphql',
82
+ protocol: 'graphql',
83
+ intent: 'read',
84
+ request: { schema: { type: 'object' } },
85
+ response: { schema: { type: 'object' } },
86
+ metadata: {
87
+ document: 'query Viewer($id: ID!) { viewer(id: $id) { id } }',
88
+ selectionPath: '$.data.viewer',
89
+ },
90
+ },
91
+ },
92
+ },
93
+ },
94
+ };
95
+ }
96
+
97
+ describe('endpoint test runner', () => {
98
+ it('builds a dry-run REST request with path, query, and credential headers', async () => {
99
+ const result = await buildEndpointTestRequest({
100
+ dataSource: createRestDataSource(),
101
+ endpointId: 'posts',
102
+ operationId: 'posts.get',
103
+ dryRun: true,
104
+ values: {
105
+ postId: 'post 1',
106
+ preview: true,
107
+ },
108
+ credentialResolver: () => ({
109
+ headers: { authorization: 'Bearer token' },
110
+ }),
111
+ });
112
+
113
+ expect(result.ok).toBe(true);
114
+ if (result.ok) {
115
+ expect(result.request.url).toBe('https://cms.example.com/posts/post%201?preview=true');
116
+ expect(result.request.method).toBe('GET');
117
+ expect(result.request.headers.authorization).toBe('Bearer token');
118
+ expect(result.request.dryRun).toBe(true);
119
+ }
120
+ });
121
+
122
+ it('executes a successful REST request and parses JSON responses', async () => {
123
+ const fetch: EndpointTestFetch = (url, init) => {
124
+ expect(url).toBe('https://cms.example.com/posts/post-1');
125
+ expect(init.method).toBe('GET');
126
+ return Promise.resolve({
127
+ status: 200,
128
+ headers: { 'content-type': 'application/json' },
129
+ text: () => Promise.resolve('{"id":"post-1"}'),
130
+ });
131
+ };
132
+
133
+ const result = await testEndpoint({
134
+ dataSource: createRestDataSource(),
135
+ endpointId: 'posts',
136
+ operationId: 'posts.get',
137
+ values: { postId: 'post-1' },
138
+ credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
139
+ fetch,
140
+ });
141
+
142
+ expect(result.ok).toBe(true);
143
+ if (result.ok) {
144
+ expect(result.response?.status).toBe(200);
145
+ expect(result.data).toEqual({ id: 'post-1' });
146
+ }
147
+ });
148
+
149
+ it('returns structured diagnostics for HTTP errors', async () => {
150
+ const fetch: EndpointTestFetch = () =>
151
+ Promise.resolve({
152
+ status: 404,
153
+ text: () => Promise.resolve('{"error":"Not found"}'),
154
+ });
155
+
156
+ const result = await testEndpoint({
157
+ dataSource: createRestDataSource(),
158
+ endpointId: 'posts',
159
+ operationId: 'posts.get',
160
+ values: { postId: 'missing' },
161
+ credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
162
+ fetch,
163
+ });
164
+
165
+ expect(result.ok).toBe(false);
166
+ if (!result.ok) {
167
+ expect(result.response?.status).toBe(404);
168
+ expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toContain('http-error');
169
+ }
170
+ });
171
+
172
+ it('returns structured diagnostics for network errors', async () => {
173
+ const fetch: EndpointTestFetch = () => Promise.reject(new Error('Connection failed'));
174
+
175
+ const result = await testEndpoint({
176
+ dataSource: createRestDataSource(),
177
+ endpointId: 'posts',
178
+ operationId: 'posts.get',
179
+ values: { postId: 'post-1' },
180
+ credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
181
+ fetch,
182
+ });
183
+
184
+ expect(result.ok).toBe(false);
185
+ if (!result.ok) {
186
+ expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toContain('network-error');
187
+ expect(result.diagnostics.map((diagnostic) => diagnostic.message)).toContain(
188
+ 'Connection failed',
189
+ );
190
+ }
191
+ });
192
+
193
+ it('returns structured diagnostics for missing credentials', async () => {
194
+ const result = await buildEndpointTestRequest({
195
+ dataSource: createRestDataSource(),
196
+ endpointId: 'posts',
197
+ operationId: 'posts.get',
198
+ values: { postId: 'post-1' },
199
+ });
200
+
201
+ expect(result.ok).toBe(false);
202
+ if (!result.ok) {
203
+ expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toContain(
204
+ 'missing-credential',
205
+ );
206
+ }
207
+ });
208
+
209
+ it('returns structured diagnostics for invalid input', async () => {
210
+ const result = await buildEndpointTestRequest({
211
+ dataSource: createRestDataSource(),
212
+ endpointId: 'posts',
213
+ operationId: 'posts.get',
214
+ credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
215
+ });
216
+
217
+ expect(result.ok).toBe(false);
218
+ if (!result.ok) {
219
+ expect(result.diagnostics.map((diagnostic) => diagnostic.message)).toContain(
220
+ "Missing required path parameter 'postId'.",
221
+ );
222
+ }
223
+ });
224
+
225
+ it('builds POST request bodies from input body values', async () => {
226
+ const result = await buildEndpointTestRequest({
227
+ dataSource: createRestDataSource(),
228
+ endpointId: 'posts',
229
+ operationId: 'posts.create',
230
+ dryRun: true,
231
+ values: {
232
+ body: { title: 'Hello' },
233
+ },
234
+ credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
235
+ });
236
+
237
+ expect(result.ok).toBe(true);
238
+ if (result.ok) {
239
+ expect(result.request.method).toBe('POST');
240
+ expect(result.request.body).toBe('{"title":"Hello"}');
241
+ expect(result.request.headers['content-type']).toBe('application/json');
242
+ }
243
+ });
244
+
245
+ it('builds dry-run GraphQL requests from operation metadata', async () => {
246
+ const result = await buildEndpointTestRequest({
247
+ dataSource: createGraphQlDataSource(),
248
+ endpointId: 'graphql',
249
+ operationId: 'query.viewer',
250
+ dryRun: true,
251
+ values: {
252
+ variables: { id: 'viewer-1' },
253
+ },
254
+ });
255
+
256
+ expect(result.ok).toBe(true);
257
+ if (result.ok) {
258
+ expect(result.request.url).toBe('https://content.example.com/graphql');
259
+ expect(result.request.method).toBe('POST');
260
+ expect(result.request.headers['content-type']).toBe('application/json');
261
+ expect(result.request.body).toBe(
262
+ '{"query":"query Viewer($id: ID!) { viewer(id: $id) { id } }","variables":{"id":"viewer-1"}}',
263
+ );
264
+ }
265
+ });
266
+ });
@@ -0,0 +1,533 @@
1
+ import type {
2
+ CredentialRef,
3
+ DataContractValue,
4
+ DataEndpointConfig,
5
+ DataOperationConfig,
6
+ DataOperationParameter,
7
+ DataSourceConfig,
8
+ DataSourceDiagnostic,
9
+ EndpointId,
10
+ OperationId,
11
+ } from '@ankhorage/contracts/data';
12
+
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
+ }
70
+
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
+ };
85
+
86
+ interface EndpointOperationSelection {
87
+ readonly endpoint: DataEndpointConfig;
88
+ readonly operation: DataOperationConfig;
89
+ }
90
+
91
+ export async function testEndpoint(input: EndpointTestInput): Promise<EndpointTestResult> {
92
+ const built = await buildEndpointTestRequest(input);
93
+
94
+ if (!built.ok) return built;
95
+ if (input.dryRun === true) return built;
96
+
97
+ if (input.fetch === undefined) {
98
+ return {
99
+ ok: false,
100
+ request: built.request,
101
+ diagnostics: [
102
+ ...built.diagnostics,
103
+ createDiagnostic(
104
+ input,
105
+ 'invalid-config',
106
+ 'Endpoint test runner requires an injected fetch function.',
107
+ ),
108
+ ],
109
+ };
110
+ }
111
+
112
+ try {
113
+ const response = await input.fetch(built.request.url, {
114
+ method: built.request.method,
115
+ headers: built.request.headers,
116
+ body: built.request.body,
117
+ });
118
+ const responseDiagnostic = await createResponseDiagnostic(response);
119
+ const diagnostics = [...built.diagnostics];
120
+
121
+ if (!responseDiagnostic.ok) {
122
+ diagnostics.push(
123
+ createDiagnostic(
124
+ input,
125
+ 'http-error',
126
+ `Endpoint test request returned HTTP status ${responseDiagnostic.status}.`,
127
+ ),
128
+ );
129
+ }
130
+
131
+ return responseDiagnostic.ok
132
+ ? {
133
+ ok: true,
134
+ request: built.request,
135
+ response: responseDiagnostic,
136
+ data: responseDiagnostic.parsedBody,
137
+ diagnostics,
138
+ }
139
+ : {
140
+ ok: false,
141
+ request: built.request,
142
+ response: responseDiagnostic,
143
+ diagnostics,
144
+ };
145
+ } catch (error) {
146
+ return {
147
+ ok: false,
148
+ request: built.request,
149
+ diagnostics: [
150
+ ...built.diagnostics,
151
+ createDiagnostic(input, 'network-error', createNetworkErrorMessage(error)),
152
+ ],
153
+ };
154
+ }
155
+ }
156
+
157
+ export async function buildEndpointTestRequest(
158
+ input: EndpointTestInput,
159
+ ): Promise<EndpointTestResult> {
160
+ const selection = selectEndpointOperation(input);
161
+ if (selection === undefined) {
162
+ return {
163
+ ok: false,
164
+ diagnostics: [
165
+ createDiagnostic(input, 'missing-operation', 'Endpoint or operation could not be found.'),
166
+ ],
167
+ };
168
+ }
169
+
170
+ const diagnostics: DataSourceDiagnostic[] = [];
171
+ const credential = await resolveEndpointCredential(input, selection, diagnostics);
172
+ const request =
173
+ selection.operation.protocol === 'graphql'
174
+ ? buildGraphQlRequest(input, selection, credential, diagnostics)
175
+ : buildHttpRequest(input, selection, credential, diagnostics);
176
+
177
+ if (request === undefined || diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
178
+ return {
179
+ ok: false,
180
+ request,
181
+ diagnostics,
182
+ };
183
+ }
184
+
185
+ return {
186
+ ok: true,
187
+ request,
188
+ diagnostics,
189
+ };
190
+ }
191
+
192
+ function selectEndpointOperation(input: EndpointTestInput): EndpointOperationSelection | undefined {
193
+ const endpoint = input.dataSource.endpoints[input.endpointId];
194
+ const operation = endpoint?.operations[input.operationId];
195
+
196
+ if (endpoint === undefined || operation === undefined) return undefined;
197
+
198
+ return { endpoint, operation };
199
+ }
200
+
201
+ async function resolveEndpointCredential(
202
+ input: EndpointTestInput,
203
+ selection: EndpointOperationSelection,
204
+ diagnostics: DataSourceDiagnostic[],
205
+ ): Promise<EndpointTestCredential | undefined> {
206
+ const credentialRef =
207
+ selection.operation.credential ?? selection.endpoint.credential ?? input.dataSource.credential;
208
+
209
+ if (credentialRef === undefined) return undefined;
210
+
211
+ if (input.credentialResolver === undefined) {
212
+ diagnostics.push(
213
+ createDiagnostic(
214
+ input,
215
+ 'missing-credential',
216
+ `Credential '${credentialRef.id}' requires a resolver.`,
217
+ ),
218
+ );
219
+ return undefined;
220
+ }
221
+
222
+ const credential = await input.credentialResolver(credentialRef);
223
+
224
+ if (credential === undefined) {
225
+ diagnostics.push(
226
+ createDiagnostic(
227
+ input,
228
+ 'missing-credential',
229
+ `Credential '${credentialRef.id}' could not be resolved.`,
230
+ ),
231
+ );
232
+ }
233
+
234
+ return credential;
235
+ }
236
+
237
+ function buildHttpRequest(
238
+ input: EndpointTestInput,
239
+ selection: EndpointOperationSelection,
240
+ credential: EndpointTestCredential | undefined,
241
+ diagnostics: DataSourceDiagnostic[],
242
+ ): EndpointTestRequestDiagnostic | undefined {
243
+ const baseUrl = getDataSourceBaseUrl(input.dataSource, selection.endpoint);
244
+ const path = selection.operation.path ?? selection.endpoint.path;
245
+
246
+ if (baseUrl === undefined || baseUrl.trim().length === 0) {
247
+ diagnostics.push(
248
+ createDiagnostic(input, 'invalid-config', 'HTTP endpoint requires a base URL.'),
249
+ );
250
+ return undefined;
251
+ }
252
+
253
+ if (path === undefined) {
254
+ diagnostics.push(createDiagnostic(input, 'invalid-config', 'HTTP operation requires a path.'));
255
+ return undefined;
256
+ }
257
+
258
+ const values = input.values ?? {};
259
+ const parameters = selection.operation.request?.parameters ?? [];
260
+ const pathResult = interpolatePath(path, parameters, values, input, diagnostics);
261
+ const query = collectQuery(parameters, values, credential?.query);
262
+ const url = appendQuery(joinUrl(baseUrl, pathResult), query);
263
+ const body = createHttpBody(selection.operation, values);
264
+ const headers: Record<string, string> = {
265
+ ...(credential?.headers ?? {}),
266
+ };
267
+
268
+ if (body !== undefined) {
269
+ headers['content-type'] = headers['content-type'] ?? 'application/json';
270
+ }
271
+
272
+ return {
273
+ dataSourceId: input.dataSource.id,
274
+ endpointId: input.endpointId,
275
+ operationId: input.operationId,
276
+ url,
277
+ method: selection.operation.method ?? 'GET',
278
+ headers,
279
+ body,
280
+ dryRun: input.dryRun === true,
281
+ };
282
+ }
283
+
284
+ function buildGraphQlRequest(
285
+ input: EndpointTestInput,
286
+ selection: EndpointOperationSelection,
287
+ credential: EndpointTestCredential | undefined,
288
+ diagnostics: DataSourceDiagnostic[],
289
+ ): EndpointTestRequestDiagnostic | undefined {
290
+ const endpointUrl = getGraphQlEndpointUrl(input.dataSource, selection.endpoint);
291
+
292
+ if (endpointUrl === undefined || endpointUrl.trim().length === 0) {
293
+ diagnostics.push(
294
+ createDiagnostic(input, 'invalid-config', 'GraphQL endpoint requires an endpoint URL.'),
295
+ );
296
+ return undefined;
297
+ }
298
+
299
+ const query =
300
+ getStringMetadataValue(selection.operation.metadata, 'document') ??
301
+ getStringValue(input.values, 'query');
302
+
303
+ if (query === undefined) {
304
+ diagnostics.push(
305
+ createDiagnostic(input, 'invalid-config', 'GraphQL operation requires a query document.'),
306
+ );
307
+ return undefined;
308
+ }
309
+
310
+ const variables = getRecordValue(input.values, 'variables');
311
+ const body = JSON.stringify({ query, variables: variables ?? {} });
312
+ const headers: Record<string, string> = {
313
+ ...(credential?.headers ?? {}),
314
+ 'content-type': 'application/json',
315
+ };
316
+
317
+ return {
318
+ dataSourceId: input.dataSource.id,
319
+ endpointId: input.endpointId,
320
+ operationId: input.operationId,
321
+ url: appendQuery(endpointUrl, credential?.query ?? {}),
322
+ method: 'POST',
323
+ headers,
324
+ body,
325
+ dryRun: input.dryRun === true,
326
+ };
327
+ }
328
+
329
+ function getDataSourceBaseUrl(
330
+ dataSource: DataSourceConfig,
331
+ endpoint: DataEndpointConfig,
332
+ ): string | undefined {
333
+ if (endpoint.baseUrl !== undefined) return endpoint.baseUrl;
334
+ if (dataSource.kind === 'rest' || dataSource.kind === 'openapi') return dataSource.baseUrl;
335
+ if (dataSource.kind === 'graphql') return dataSource.endpointUrl;
336
+ return undefined;
337
+ }
338
+
339
+ function getGraphQlEndpointUrl(
340
+ dataSource: DataSourceConfig,
341
+ endpoint: DataEndpointConfig,
342
+ ): string | undefined {
343
+ if (endpoint.baseUrl !== undefined) return endpoint.baseUrl;
344
+ if (dataSource.kind === 'graphql') return dataSource.endpointUrl;
345
+ return undefined;
346
+ }
347
+
348
+ function interpolatePath(
349
+ path: string,
350
+ parameters: readonly DataOperationParameter[],
351
+ values: EndpointTestInputValues,
352
+ input: EndpointTestInput,
353
+ diagnostics: DataSourceDiagnostic[],
354
+ ): string {
355
+ let nextPath = path;
356
+ const pathParameters = parameters.filter((parameter) => parameter.location === 'path');
357
+
358
+ for (const parameter of pathParameters) {
359
+ const value = values[parameter.name] ?? parameter.default;
360
+
361
+ if (value === undefined) {
362
+ diagnostics.push(
363
+ createDiagnostic(
364
+ input,
365
+ 'invalid-config',
366
+ `Missing required path parameter '${parameter.name}'.`,
367
+ ),
368
+ );
369
+ continue;
370
+ }
371
+
372
+ const encodedValue = encodeURIComponent(serializeEndpointUrlValue(value));
373
+ nextPath = nextPath.replaceAll(`{${parameter.name}}`, encodedValue);
374
+ nextPath = nextPath.replaceAll(`:${parameter.name}`, encodedValue);
375
+ }
376
+
377
+ return nextPath;
378
+ }
379
+
380
+ function collectQuery(
381
+ parameters: readonly DataOperationParameter[],
382
+ values: EndpointTestInputValues,
383
+ credentialQuery: EndpointTestInputValues | undefined,
384
+ ): EndpointTestInputValues {
385
+ const query: Record<string, DataContractValue> = { ...(credentialQuery ?? {}) };
386
+
387
+ for (const parameter of parameters) {
388
+ if (parameter.location !== 'query') continue;
389
+
390
+ const value = values[parameter.name] ?? parameter.default;
391
+ if (value !== undefined) query[parameter.name] = value;
392
+ }
393
+
394
+ return query;
395
+ }
396
+
397
+ function createHttpBody(
398
+ operation: DataOperationConfig,
399
+ values: EndpointTestInputValues,
400
+ ): string | undefined {
401
+ if (operation.method === 'GET' || operation.method === 'HEAD') return undefined;
402
+
403
+ const { body } = values;
404
+ if (body === undefined) return undefined;
405
+
406
+ return JSON.stringify(body);
407
+ }
408
+
409
+ async function createResponseDiagnostic(
410
+ response: EndpointTestFetchResponse,
411
+ ): Promise<EndpointTestResponseDiagnostic> {
412
+ const bodyText = await response.text();
413
+ const parsedBody = parseResponseBody(bodyText);
414
+
415
+ return {
416
+ status: response.status,
417
+ ok: response.status >= 200 && response.status < 300,
418
+ headers: response.headers,
419
+ bodyText,
420
+ parsedBody,
421
+ };
422
+ }
423
+
424
+ function parseResponseBody(bodyText: string): DataContractValue | undefined {
425
+ if (bodyText.trim().length === 0) return undefined;
426
+
427
+ try {
428
+ const parsed: unknown = JSON.parse(bodyText);
429
+ return isDataContractValue(parsed) ? parsed : { raw: bodyText };
430
+ } catch {
431
+ return { raw: bodyText };
432
+ }
433
+ }
434
+
435
+ function appendQuery(url: string, query: EndpointTestInputValues): string {
436
+ const entries = Object.entries(query);
437
+ if (entries.length === 0) return url;
438
+
439
+ const search = entries
440
+ .map(
441
+ ([key, value]) =>
442
+ `${encodeURIComponent(key)}=${encodeURIComponent(serializeEndpointUrlValue(value))}`,
443
+ )
444
+ .join('&');
445
+ const separator = url.includes('?') ? '&' : '?';
446
+
447
+ return `${url}${separator}${search}`;
448
+ }
449
+
450
+ function serializeEndpointUrlValue(value: DataContractValue): string {
451
+ if (typeof value === 'string') return value;
452
+ if (typeof value === 'number' || typeof value === 'boolean') return value.toString();
453
+ if (value === null) return '';
454
+
455
+ return JSON.stringify(value);
456
+ }
457
+
458
+ function joinUrl(baseUrl: string, path: string): string {
459
+ const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
460
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
461
+
462
+ return `${normalizedBase}${normalizedPath}`;
463
+ }
464
+
465
+ function getStringValue(
466
+ values: EndpointTestInputValues | undefined,
467
+ key: string,
468
+ ): string | undefined {
469
+ const value = values?.[key];
470
+ return typeof value === 'string' ? value : undefined;
471
+ }
472
+
473
+ function getRecordValue(
474
+ values: EndpointTestInputValues | undefined,
475
+ key: string,
476
+ ): DataContractValue | undefined {
477
+ const value = values?.[key];
478
+ return isDataContractValue(value) ? value : undefined;
479
+ }
480
+
481
+ function getStringMetadataValue(
482
+ metadata: DataContractValue | undefined,
483
+ key: string,
484
+ ): string | undefined {
485
+ if (!isDataContractRecord(metadata)) return undefined;
486
+
487
+ const value = metadata[key];
488
+ return typeof value === 'string' ? value : undefined;
489
+ }
490
+
491
+ function createDiagnostic(
492
+ input: EndpointTestInput,
493
+ code: string,
494
+ message: string,
495
+ ): DataSourceDiagnostic {
496
+ return {
497
+ code,
498
+ dataSourceId: input.dataSource.id,
499
+ endpointId: input.endpointId,
500
+ operationId: input.operationId,
501
+ message,
502
+ severity: code === 'missing-schema' ? 'info' : 'error',
503
+ };
504
+ }
505
+
506
+ function createNetworkErrorMessage(error: unknown): string {
507
+ if (error instanceof Error) return error.message;
508
+ return 'Endpoint test request failed.';
509
+ }
510
+
511
+ function isDataContractValue(value: unknown): value is DataContractValue {
512
+ if (
513
+ value === null ||
514
+ typeof value === 'string' ||
515
+ typeof value === 'number' ||
516
+ typeof value === 'boolean'
517
+ ) {
518
+ return true;
519
+ }
520
+
521
+ if (Array.isArray(value)) return value.every(isDataContractValue);
522
+ if (typeof value !== 'object') return false;
523
+
524
+ return Object.values(value).every(isDataContractValue);
525
+ }
526
+
527
+ function isDataContractRecord(
528
+ value: DataContractValue | undefined,
529
+ ): value is Record<string, DataContractValue> {
530
+ return (
531
+ value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value)
532
+ );
533
+ }