@ankhorage/data-sources 1.0.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +6 -41
- package/dist/ankh.provider.js +1 -1
- package/dist/ankh.provider.js.map +1 -1
- package/dist/discovery/http.d.ts +10 -0
- package/dist/discovery/http.d.ts.map +1 -0
- package/dist/discovery/http.js +35 -0
- package/dist/discovery/http.js.map +1 -0
- package/dist/discovery/index.d.ts +9 -9
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +27 -55
- package/dist/discovery/index.js.map +1 -1
- package/dist/graphql/index.d.ts +8 -76
- package/dist/graphql/index.d.ts.map +1 -1
- package/dist/graphql/index.js +17 -197
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/introspection.d.ts +5 -0
- package/dist/graphql/introspection.d.ts.map +1 -0
- package/dist/graphql/introspection.js +136 -0
- package/dist/graphql/introspection.js.map +1 -0
- package/dist/graphql/operation.d.ts +5 -0
- package/dist/graphql/operation.d.ts.map +1 -0
- package/dist/graphql/operation.js +39 -0
- package/dist/graphql/operation.js.map +1 -0
- package/dist/graphql/types.d.ts +71 -0
- package/dist/graphql/types.d.ts.map +1 -0
- package/dist/graphql/types.js +2 -0
- package/dist/graphql/types.js.map +1 -0
- package/dist/index.d.ts +1 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -34
- package/dist/index.js.map +1 -1
- package/dist/openapi/endpoints.d.ts +5 -0
- package/dist/openapi/endpoints.d.ts.map +1 -0
- package/dist/openapi/endpoints.js +77 -0
- package/dist/openapi/endpoints.js.map +1 -0
- package/dist/openapi/index.d.ts +7 -87
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +8 -267
- package/dist/openapi/index.js.map +1 -1
- package/dist/openapi/operation.d.ts +5 -0
- package/dist/openapi/operation.d.ts.map +1 -0
- package/dist/openapi/operation.js +141 -0
- package/dist/openapi/operation.js.map +1 -0
- package/dist/openapi/schema.d.ts +5 -0
- package/dist/openapi/schema.d.ts.map +1 -0
- package/dist/openapi/schema.js +84 -0
- package/dist/openapi/schema.js.map +1 -0
- package/dist/openapi/types.d.ts +82 -0
- package/dist/openapi/types.d.ts.map +1 -0
- package/dist/openapi/types.js +2 -0
- package/dist/openapi/types.js.map +1 -0
- package/dist/packageInfo.d.ts +18 -0
- package/dist/packageInfo.d.ts.map +1 -0
- package/dist/packageInfo.js +30 -0
- package/dist/packageInfo.js.map +1 -0
- package/dist/rest/index.d.ts +5 -5
- package/dist/rest/index.d.ts.map +1 -1
- package/dist/rest/index.js +49 -48
- package/dist/rest/index.js.map +1 -1
- package/dist/test-runner/index.d.ts +2 -57
- package/dist/test-runner/index.d.ts.map +1 -1
- package/dist/test-runner/index.js +58 -160
- package/dist/test-runner/index.js.map +1 -1
- package/dist/test-runner/response.d.ts +3 -0
- package/dist/test-runner/response.d.ts.map +1 -0
- package/dist/test-runner/response.js +29 -0
- package/dist/test-runner/response.js.map +1 -0
- package/dist/test-runner/result.d.ts +6 -0
- package/dist/test-runner/result.d.ts.map +1 -0
- package/dist/test-runner/result.js +17 -0
- package/dist/test-runner/result.js.map +1 -0
- package/dist/test-runner/serialization.d.ts +12 -0
- package/dist/test-runner/serialization.d.ts.map +1 -0
- package/dist/test-runner/serialization.js +99 -0
- package/dist/test-runner/serialization.js.map +1 -0
- package/dist/test-runner/types.d.ts +58 -0
- package/dist/test-runner/types.d.ts.map +1 -0
- package/dist/test-runner/types.js +2 -0
- package/dist/test-runner/types.js.map +1 -0
- package/package.json +12 -13
- package/src/ankh.provider.ts +1 -1
- package/src/api-phase1.contract.test.ts +24 -0
- package/src/discovery/http.ts +38 -0
- package/src/discovery/index.test.ts +76 -75
- package/src/discovery/index.ts +49 -72
- package/src/graphql/index.test.ts +10 -11
- package/src/graphql/index.ts +35 -315
- package/src/graphql/introspection.ts +163 -0
- package/src/graphql/operation.ts +60 -0
- package/src/graphql/types.ts +79 -0
- package/src/index.test.ts +21 -13
- package/src/index.ts +1 -54
- package/src/openapi/endpoints.ts +124 -0
- package/src/openapi/index.test.ts +47 -47
- package/src/openapi/index.ts +12 -467
- package/src/openapi/operation.ts +231 -0
- package/src/openapi/schema.ts +127 -0
- package/src/openapi/types.ts +93 -0
- package/src/packageInfo.ts +50 -0
- package/src/rest/index.test.ts +17 -14
- package/src/rest/index.ts +67 -61
- package/src/test-runner/index.test.ts +153 -120
- package/src/test-runner/index.ts +97 -299
- package/src/test-runner/response.ts +32 -0
- package/src/test-runner/result.ts +35 -0
- package/src/test-runner/serialization.ts +151 -0
- package/src/test-runner/types.ts +81 -0
- package/dist/generated-api/index.d.ts +0 -10
- package/dist/generated-api/index.d.ts.map +0 -1
- package/dist/generated-api/index.js +0 -274
- package/dist/generated-api/index.js.map +0 -1
- package/dist/generated-api/validateGeneratedApiCollection.d.ts +0 -3
- package/dist/generated-api/validateGeneratedApiCollection.d.ts.map +0 -1
- package/dist/generated-api/validateGeneratedApiCollection.js +0 -35
- package/dist/generated-api/validateGeneratedApiCollection.js.map +0 -1
- package/src/generated-api/index.test.ts +0 -173
- package/src/generated-api/index.ts +0 -414
- package/src/generated-api/validateGeneratedApiCollection.test.ts +0 -91
- package/src/generated-api/validateGeneratedApiCollection.ts +0 -76
package/src/openapi/index.ts
CHANGED
|
@@ -1,127 +1,19 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
CredentialRef,
|
|
3
|
-
DataContractValue,
|
|
4
|
-
DataEndpointConfig,
|
|
5
|
-
DataOperationConfig,
|
|
6
|
-
DataOperationIntent,
|
|
7
|
-
DataOperationParameter,
|
|
8
|
-
DataOperationParameterLocation,
|
|
9
|
-
DataOperationRequest,
|
|
10
|
-
DataOperationResponse,
|
|
11
|
-
DataSchema,
|
|
12
|
-
DataSchemaPrimitiveType,
|
|
13
|
-
DataSchemaRegistry,
|
|
14
2
|
DataSourceDiagnostic,
|
|
15
3
|
DataSourceDiagnosticResult,
|
|
16
|
-
|
|
17
|
-
ExternalRestApiDataSourceConfig,
|
|
18
|
-
OperationId,
|
|
4
|
+
ExternalRestApiDefinition,
|
|
19
5
|
} from '@ankhorage/contracts/data';
|
|
20
6
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'boolean',
|
|
25
|
-
'integer',
|
|
26
|
-
'null',
|
|
27
|
-
'number',
|
|
28
|
-
'object',
|
|
29
|
-
'string',
|
|
30
|
-
] as const satisfies readonly DataSchemaPrimitiveType[];
|
|
7
|
+
import { normalizeOpenApiEndpoints } from './endpoints';
|
|
8
|
+
import { normalizeOpenApiSchemas } from './schema';
|
|
9
|
+
import type { OpenApiImportInput } from './types';
|
|
31
10
|
|
|
32
|
-
export
|
|
11
|
+
export { normalizeOpenApiEndpointId } from './endpoints';
|
|
12
|
+
export { normalizeOpenApiOperationId } from './operation';
|
|
13
|
+
export { normalizeOpenApiSchema } from './schema';
|
|
14
|
+
export type * from './types';
|
|
33
15
|
|
|
34
|
-
export
|
|
35
|
-
readonly url: string;
|
|
36
|
-
readonly description?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface OpenApiSchemaObject {
|
|
40
|
-
readonly $ref?: string;
|
|
41
|
-
readonly type?: string | readonly string[];
|
|
42
|
-
readonly title?: string;
|
|
43
|
-
readonly description?: string;
|
|
44
|
-
readonly enum?: readonly DataContractValue[];
|
|
45
|
-
readonly const?: DataContractValue;
|
|
46
|
-
readonly default?: DataContractValue;
|
|
47
|
-
readonly format?: string;
|
|
48
|
-
readonly nullable?: boolean;
|
|
49
|
-
readonly required?: readonly string[];
|
|
50
|
-
readonly properties?: Readonly<Record<string, OpenApiSchemaObject>>;
|
|
51
|
-
readonly additionalProperties?: boolean | OpenApiSchemaObject;
|
|
52
|
-
readonly items?: OpenApiSchemaObject;
|
|
53
|
-
readonly allOf?: readonly OpenApiSchemaObject[];
|
|
54
|
-
readonly anyOf?: readonly OpenApiSchemaObject[];
|
|
55
|
-
readonly oneOf?: readonly OpenApiSchemaObject[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface OpenApiMediaTypeObject {
|
|
59
|
-
readonly schema?: OpenApiSchemaObject;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface OpenApiRequestBodyObject {
|
|
63
|
-
readonly description?: string;
|
|
64
|
-
readonly required?: boolean;
|
|
65
|
-
readonly content?: Readonly<Record<string, OpenApiMediaTypeObject>>;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface OpenApiResponseObject {
|
|
69
|
-
readonly description?: string;
|
|
70
|
-
readonly content?: Readonly<Record<string, OpenApiMediaTypeObject>>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface OpenApiParameterObject {
|
|
74
|
-
readonly name: string;
|
|
75
|
-
readonly in: string;
|
|
76
|
-
readonly required?: boolean;
|
|
77
|
-
readonly description?: string;
|
|
78
|
-
readonly schema?: OpenApiSchemaObject;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface OpenApiOperationObject {
|
|
82
|
-
readonly operationId?: string;
|
|
83
|
-
readonly summary?: string;
|
|
84
|
-
readonly description?: string;
|
|
85
|
-
readonly deprecated?: boolean;
|
|
86
|
-
readonly parameters?: readonly OpenApiParameterObject[];
|
|
87
|
-
readonly requestBody?: OpenApiRequestBodyObject;
|
|
88
|
-
readonly responses?: Readonly<Record<string, OpenApiResponseObject>>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export type OpenApiPathItemObject = Partial<Record<OpenApiHttpMethod, OpenApiOperationObject>> & {
|
|
92
|
-
readonly parameters?: readonly OpenApiParameterObject[];
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
export interface OpenApiComponentsObject {
|
|
96
|
-
readonly schemas?: Readonly<Record<string, OpenApiSchemaObject>>;
|
|
97
|
-
readonly securitySchemes?: Readonly<Record<string, DataContractValue>>;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface OpenApiDocumentObject {
|
|
101
|
-
readonly openapi?: string;
|
|
102
|
-
readonly info?: {
|
|
103
|
-
readonly title?: string;
|
|
104
|
-
readonly version?: string;
|
|
105
|
-
readonly description?: string;
|
|
106
|
-
};
|
|
107
|
-
readonly servers?: readonly OpenApiServerObject[];
|
|
108
|
-
readonly paths?: Readonly<Record<string, OpenApiPathItemObject>>;
|
|
109
|
-
readonly components?: OpenApiComponentsObject;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface OpenApiImportInput {
|
|
113
|
-
readonly id: string;
|
|
114
|
-
readonly document: OpenApiDocumentObject;
|
|
115
|
-
readonly baseUrl?: string;
|
|
116
|
-
readonly credential?: CredentialRef;
|
|
117
|
-
readonly documentId?: string;
|
|
118
|
-
readonly documentUrl?: string;
|
|
119
|
-
readonly name?: string;
|
|
120
|
-
readonly description?: string;
|
|
121
|
-
readonly metadata?: DataContractValue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export type OpenApiImportResult = DataSourceDiagnosticResult<ExternalRestApiDataSourceConfig>;
|
|
16
|
+
export type OpenApiImportResult = DataSourceDiagnosticResult<ExternalRestApiDefinition>;
|
|
125
17
|
|
|
126
18
|
export function importOpenApiDocument(input: OpenApiImportInput): OpenApiImportResult {
|
|
127
19
|
const diagnostics: DataSourceDiagnostic[] = [];
|
|
@@ -131,7 +23,7 @@ export function importOpenApiDocument(input: OpenApiImportInput): OpenApiImportR
|
|
|
131
23
|
input.id,
|
|
132
24
|
diagnostics,
|
|
133
25
|
);
|
|
134
|
-
const endpoints = normalizeOpenApiEndpoints(input,
|
|
26
|
+
const endpoints = normalizeOpenApiEndpoints(input, diagnostics);
|
|
135
27
|
|
|
136
28
|
if (baseUrl === undefined || diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
|
137
29
|
return { ok: false, diagnostics };
|
|
@@ -141,7 +33,6 @@ export function importOpenApiDocument(input: OpenApiImportInput): OpenApiImportR
|
|
|
141
33
|
ok: true,
|
|
142
34
|
data: {
|
|
143
35
|
id: input.id,
|
|
144
|
-
kind: 'api',
|
|
145
36
|
origin: 'external',
|
|
146
37
|
protocol: 'rest',
|
|
147
38
|
name: input.name ?? input.document.info?.title,
|
|
@@ -161,50 +52,6 @@ export function importOpenApiDocument(input: OpenApiImportInput): OpenApiImportR
|
|
|
161
52
|
};
|
|
162
53
|
}
|
|
163
54
|
|
|
164
|
-
export function normalizeOpenApiOperationId(
|
|
165
|
-
method: OpenApiHttpMethod,
|
|
166
|
-
path: string,
|
|
167
|
-
operationId?: string,
|
|
168
|
-
): OperationId {
|
|
169
|
-
const rawId = operationId ?? `${method}-${path}`;
|
|
170
|
-
const normalized = rawId
|
|
171
|
-
.replace(/\{([^}]+)\}/g, '$1')
|
|
172
|
-
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
173
|
-
.replace(/^-+|-+$/g, '')
|
|
174
|
-
.toLowerCase();
|
|
175
|
-
return normalized.length > 0 ? normalized : `${method}-root`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function normalizeOpenApiEndpointId(path: string): EndpointId {
|
|
179
|
-
const normalized = path
|
|
180
|
-
.replace(/\{([^}]+)\}/g, '$1')
|
|
181
|
-
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
182
|
-
.replace(/^-+|-+$/g, '')
|
|
183
|
-
.toLowerCase();
|
|
184
|
-
return normalized.length > 0 ? normalized : 'root';
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export function normalizeOpenApiSchema(schema: OpenApiSchemaObject): DataSchema {
|
|
188
|
-
return {
|
|
189
|
-
ref: normalizeOpenApiSchemaRef(schema.$ref),
|
|
190
|
-
type: normalizeSchemaType(schema.type),
|
|
191
|
-
title: schema.title,
|
|
192
|
-
description: schema.description,
|
|
193
|
-
enum: schema.enum,
|
|
194
|
-
const: schema.const,
|
|
195
|
-
default: schema.default,
|
|
196
|
-
format: schema.format,
|
|
197
|
-
nullable: schema.nullable,
|
|
198
|
-
required: schema.required,
|
|
199
|
-
properties: normalizeSchemaRecord(schema.properties),
|
|
200
|
-
additionalProperties: normalizeAdditionalProperties(schema.additionalProperties),
|
|
201
|
-
items: schema.items === undefined ? undefined : normalizeOpenApiSchema(schema.items),
|
|
202
|
-
allOf: normalizeSchemaList(schema.allOf),
|
|
203
|
-
anyOf: normalizeSchemaList(schema.anyOf),
|
|
204
|
-
oneOf: normalizeSchemaList(schema.oneOf),
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
55
|
function resolveOpenApiBaseUrl(
|
|
209
56
|
input: OpenApiImportInput,
|
|
210
57
|
diagnostics: DataSourceDiagnostic[],
|
|
@@ -215,7 +62,7 @@ function resolveOpenApiBaseUrl(
|
|
|
215
62
|
if (servers.length === 0) {
|
|
216
63
|
diagnostics.push({
|
|
217
64
|
code: 'ambiguous-server',
|
|
218
|
-
|
|
65
|
+
apiId: input.id,
|
|
219
66
|
message: 'OpenAPI import requires a server URL or an explicit baseUrl override.',
|
|
220
67
|
path: 'servers',
|
|
221
68
|
severity: 'error',
|
|
@@ -226,313 +73,11 @@ function resolveOpenApiBaseUrl(
|
|
|
226
73
|
if (servers.length > 1) {
|
|
227
74
|
diagnostics.push({
|
|
228
75
|
code: 'ambiguous-server',
|
|
229
|
-
|
|
76
|
+
apiId: input.id,
|
|
230
77
|
message: 'OpenAPI document defines multiple servers. The first server URL was selected.',
|
|
231
78
|
path: 'servers',
|
|
232
79
|
severity: 'warning',
|
|
233
80
|
});
|
|
234
81
|
}
|
|
235
|
-
|
|
236
82
|
return servers[0]?.url;
|
|
237
83
|
}
|
|
238
|
-
|
|
239
|
-
function normalizeOpenApiSchemas(
|
|
240
|
-
schemas: Readonly<Record<string, OpenApiSchemaObject>> | undefined,
|
|
241
|
-
dataSourceId: string,
|
|
242
|
-
diagnostics: DataSourceDiagnostic[],
|
|
243
|
-
): DataSchemaRegistry | undefined {
|
|
244
|
-
if (schemas === undefined) return undefined;
|
|
245
|
-
const registry: Record<string, DataSchema> = {};
|
|
246
|
-
|
|
247
|
-
for (const [schemaId, schema] of Object.entries(schemas)) {
|
|
248
|
-
registry[schemaId] = normalizeOpenApiSchema(schema);
|
|
249
|
-
collectUnsupportedSchemaDiagnostics(
|
|
250
|
-
schema,
|
|
251
|
-
dataSourceId,
|
|
252
|
-
`components.schemas.${schemaId}`,
|
|
253
|
-
diagnostics,
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
return registry;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function normalizeOpenApiEndpoints(
|
|
260
|
-
input: OpenApiImportInput,
|
|
261
|
-
baseUrl: string | undefined,
|
|
262
|
-
diagnostics: DataSourceDiagnostic[],
|
|
263
|
-
): Record<EndpointId, DataEndpointConfig> {
|
|
264
|
-
const { paths } = input.document;
|
|
265
|
-
if (paths === undefined || Object.keys(paths).length === 0) {
|
|
266
|
-
diagnostics.push({
|
|
267
|
-
code: 'invalid-config',
|
|
268
|
-
dataSourceId: input.id,
|
|
269
|
-
message: 'OpenAPI document must define at least one path.',
|
|
270
|
-
path: 'paths',
|
|
271
|
-
severity: 'error',
|
|
272
|
-
});
|
|
273
|
-
return {};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const endpoints: Record<EndpointId, DataEndpointConfig> = {};
|
|
277
|
-
const operationIds = new Set<OperationId>();
|
|
278
|
-
|
|
279
|
-
for (const [path, pathItem] of Object.entries(paths)) {
|
|
280
|
-
const endpointId = normalizeOpenApiEndpointId(path);
|
|
281
|
-
const operations: Record<OperationId, DataOperationConfig> = {};
|
|
282
|
-
|
|
283
|
-
for (const method of OPENAPI_HTTP_METHODS) {
|
|
284
|
-
const operation = pathItem[method];
|
|
285
|
-
if (operation === undefined) continue;
|
|
286
|
-
|
|
287
|
-
const operationId = resolveUniqueOperationId(
|
|
288
|
-
input.id,
|
|
289
|
-
endpointId,
|
|
290
|
-
method,
|
|
291
|
-
path,
|
|
292
|
-
operation.operationId,
|
|
293
|
-
operationIds,
|
|
294
|
-
diagnostics,
|
|
295
|
-
);
|
|
296
|
-
const parameters = normalizeOpenApiParameters(
|
|
297
|
-
[...(pathItem.parameters ?? []), ...(operation.parameters ?? [])],
|
|
298
|
-
input.id,
|
|
299
|
-
endpointId,
|
|
300
|
-
operationId,
|
|
301
|
-
diagnostics,
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
operations[operationId] = {
|
|
305
|
-
id: operationId,
|
|
306
|
-
endpointId,
|
|
307
|
-
name: operation.summary,
|
|
308
|
-
description: operation.description,
|
|
309
|
-
protocol: 'http',
|
|
310
|
-
intent: mapOpenApiMethodToIntent(method),
|
|
311
|
-
method: method.toUpperCase(),
|
|
312
|
-
path,
|
|
313
|
-
request: normalizeOpenApiRequest(operation.requestBody, parameters),
|
|
314
|
-
response: normalizeOpenApiResponse(operation.responses),
|
|
315
|
-
metadata: {
|
|
316
|
-
deprecated: operation.deprecated ?? false,
|
|
317
|
-
source: 'openapi',
|
|
318
|
-
},
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
endpoints[endpointId] = {
|
|
323
|
-
id: endpointId,
|
|
324
|
-
kind: 'http',
|
|
325
|
-
baseUrl,
|
|
326
|
-
path,
|
|
327
|
-
credential: input.credential,
|
|
328
|
-
operations,
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return endpoints;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function resolveUniqueOperationId(
|
|
336
|
-
dataSourceId: string,
|
|
337
|
-
endpointId: EndpointId,
|
|
338
|
-
method: OpenApiHttpMethod,
|
|
339
|
-
path: string,
|
|
340
|
-
operationId: string | undefined,
|
|
341
|
-
operationIds: Set<OperationId>,
|
|
342
|
-
diagnostics: DataSourceDiagnostic[],
|
|
343
|
-
): OperationId {
|
|
344
|
-
const normalized = normalizeOpenApiOperationId(method, path, operationId);
|
|
345
|
-
if (!operationIds.has(normalized)) {
|
|
346
|
-
operationIds.add(normalized);
|
|
347
|
-
return normalized;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
let suffix = 2;
|
|
351
|
-
let uniqueId: OperationId = `${normalized}-${suffix}`;
|
|
352
|
-
while (operationIds.has(uniqueId)) {
|
|
353
|
-
suffix += 1;
|
|
354
|
-
uniqueId = `${normalized}-${suffix}`;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
operationIds.add(uniqueId);
|
|
358
|
-
diagnostics.push({
|
|
359
|
-
code: 'duplicate-operation-id',
|
|
360
|
-
dataSourceId,
|
|
361
|
-
endpointId,
|
|
362
|
-
operationId: uniqueId,
|
|
363
|
-
message: `OpenAPI operation id '${normalized}' is duplicated. Normalized to '${uniqueId}'.`,
|
|
364
|
-
path: `paths.${path}.${method}.operationId`,
|
|
365
|
-
severity: 'warning',
|
|
366
|
-
});
|
|
367
|
-
return uniqueId;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function normalizeOpenApiParameters(
|
|
371
|
-
parameters: readonly OpenApiParameterObject[],
|
|
372
|
-
dataSourceId: string,
|
|
373
|
-
endpointId: EndpointId,
|
|
374
|
-
operationId: OperationId,
|
|
375
|
-
diagnostics: DataSourceDiagnostic[],
|
|
376
|
-
): readonly DataOperationParameter[] | undefined {
|
|
377
|
-
if (parameters.length === 0) return undefined;
|
|
378
|
-
const normalized: DataOperationParameter[] = [];
|
|
379
|
-
|
|
380
|
-
for (const parameter of parameters) {
|
|
381
|
-
const location = normalizeParameterLocation(parameter.in);
|
|
382
|
-
if (location === undefined) {
|
|
383
|
-
diagnostics.push({
|
|
384
|
-
code: 'invalid-config',
|
|
385
|
-
dataSourceId,
|
|
386
|
-
endpointId,
|
|
387
|
-
operationId,
|
|
388
|
-
message: `OpenAPI parameter '${parameter.name}' uses unsupported location '${parameter.in}'.`,
|
|
389
|
-
path: `paths.${endpointId}.parameters.${parameter.name}`,
|
|
390
|
-
severity: 'warning',
|
|
391
|
-
});
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
normalized.push({
|
|
396
|
-
name: parameter.name,
|
|
397
|
-
location,
|
|
398
|
-
required: parameter.required,
|
|
399
|
-
description: parameter.description,
|
|
400
|
-
schema: parameter.schema === undefined ? undefined : normalizeOpenApiSchema(parameter.schema),
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return normalized;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function normalizeParameterLocation(location: string): DataOperationParameterLocation | undefined {
|
|
408
|
-
if (
|
|
409
|
-
location === 'cookie' ||
|
|
410
|
-
location === 'header' ||
|
|
411
|
-
location === 'path' ||
|
|
412
|
-
location === 'query'
|
|
413
|
-
) {
|
|
414
|
-
return location;
|
|
415
|
-
}
|
|
416
|
-
return undefined;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function normalizeOpenApiRequest(
|
|
420
|
-
requestBody: OpenApiRequestBodyObject | undefined,
|
|
421
|
-
parameters: readonly DataOperationParameter[] | undefined,
|
|
422
|
-
): DataOperationRequest | undefined {
|
|
423
|
-
const content = firstContentEntry(requestBody?.content);
|
|
424
|
-
if (requestBody === undefined && parameters === undefined) return undefined;
|
|
425
|
-
return {
|
|
426
|
-
contentType: content?.contentType,
|
|
427
|
-
parameters,
|
|
428
|
-
schema: content?.schema === undefined ? undefined : normalizeOpenApiSchema(content.schema),
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function normalizeOpenApiResponse(
|
|
433
|
-
responses: Readonly<Record<string, OpenApiResponseObject>> | undefined,
|
|
434
|
-
): DataOperationResponse | undefined {
|
|
435
|
-
if (responses === undefined) return undefined;
|
|
436
|
-
const status = chooseResponseStatus(Object.keys(responses));
|
|
437
|
-
if (status === undefined) return undefined;
|
|
438
|
-
|
|
439
|
-
const response = responses[status];
|
|
440
|
-
const content = firstContentEntry(response?.content);
|
|
441
|
-
return {
|
|
442
|
-
status,
|
|
443
|
-
contentType: content?.contentType,
|
|
444
|
-
description: response?.description,
|
|
445
|
-
schema: content?.schema === undefined ? undefined : normalizeOpenApiSchema(content.schema),
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function firstContentEntry(
|
|
450
|
-
content: Readonly<Record<string, OpenApiMediaTypeObject>> | undefined,
|
|
451
|
-
): { readonly contentType: string; readonly schema?: OpenApiSchemaObject } | undefined {
|
|
452
|
-
if (content === undefined) return undefined;
|
|
453
|
-
const [entry] = Object.entries(content);
|
|
454
|
-
if (entry === undefined) return undefined;
|
|
455
|
-
const [contentType, media] = entry;
|
|
456
|
-
return { contentType, schema: media.schema };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function chooseResponseStatus(statuses: readonly string[]): string | undefined {
|
|
460
|
-
return statuses.find((status) => status.startsWith('2')) ?? statuses[0];
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function mapOpenApiMethodToIntent(method: OpenApiHttpMethod): DataOperationIntent {
|
|
464
|
-
if (method === 'get' || method === 'head') return 'read';
|
|
465
|
-
if (method === 'post') return 'create';
|
|
466
|
-
if (method === 'delete') return 'delete';
|
|
467
|
-
if (method === 'patch' || method === 'put') return 'update';
|
|
468
|
-
return 'action';
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function normalizeOpenApiSchemaRef(ref: string | undefined): { readonly id: string } | undefined {
|
|
472
|
-
if (ref === undefined) return undefined;
|
|
473
|
-
const prefix = '#/components/schemas/';
|
|
474
|
-
return { id: ref.startsWith(prefix) ? ref.slice(prefix.length) : ref };
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function normalizeSchemaRecord(
|
|
478
|
-
record: Readonly<Record<string, OpenApiSchemaObject>> | undefined,
|
|
479
|
-
): Readonly<Record<string, DataSchema>> | undefined {
|
|
480
|
-
if (record === undefined) return undefined;
|
|
481
|
-
return Object.fromEntries(
|
|
482
|
-
Object.entries(record).map(([key, schema]) => [key, normalizeOpenApiSchema(schema)]),
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function normalizeSchemaList(
|
|
487
|
-
list: readonly OpenApiSchemaObject[] | undefined,
|
|
488
|
-
): readonly DataSchema[] | undefined {
|
|
489
|
-
return list?.map(normalizeOpenApiSchema);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function normalizeAdditionalProperties(
|
|
493
|
-
value: boolean | OpenApiSchemaObject | undefined,
|
|
494
|
-
): boolean | DataSchema | undefined {
|
|
495
|
-
if (value === undefined || typeof value === 'boolean') return value;
|
|
496
|
-
return normalizeOpenApiSchema(value);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function normalizeSchemaType(
|
|
500
|
-
type: string | readonly string[] | undefined,
|
|
501
|
-
): DataSchemaPrimitiveType | readonly DataSchemaPrimitiveType[] | undefined {
|
|
502
|
-
if (type === undefined) return undefined;
|
|
503
|
-
if (typeof type === 'string') return isDataSchemaPrimitive(type) ? type : undefined;
|
|
504
|
-
return type.filter(isDataSchemaPrimitive);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function isDataSchemaPrimitive(type: string): type is DataSchemaPrimitiveType {
|
|
508
|
-
return DATA_SCHEMA_PRIMITIVES.some((primitive) => primitive === type);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function collectUnsupportedSchemaDiagnostics(
|
|
512
|
-
schema: OpenApiSchemaObject,
|
|
513
|
-
dataSourceId: string,
|
|
514
|
-
path: string,
|
|
515
|
-
diagnostics: DataSourceDiagnostic[],
|
|
516
|
-
): void {
|
|
517
|
-
if (typeof schema.type === 'string' && !isDataSchemaPrimitive(schema.type)) {
|
|
518
|
-
diagnostics.push({
|
|
519
|
-
code: 'unsupported-schema',
|
|
520
|
-
dataSourceId,
|
|
521
|
-
message: `OpenAPI schema type '${schema.type}' is not supported by the normalized schema model.`,
|
|
522
|
-
path: `${path}.type`,
|
|
523
|
-
severity: 'warning',
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
for (const [propertyName, propertySchema] of Object.entries(schema.properties ?? {})) {
|
|
528
|
-
collectUnsupportedSchemaDiagnostics(
|
|
529
|
-
propertySchema,
|
|
530
|
-
dataSourceId,
|
|
531
|
-
`${path}.properties.${propertyName}`,
|
|
532
|
-
diagnostics,
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
if (schema.items !== undefined) {
|
|
536
|
-
collectUnsupportedSchemaDiagnostics(schema.items, dataSourceId, `${path}.items`, diagnostics);
|
|
537
|
-
}
|
|
538
|
-
}
|