@ankhorage/data-sources 0.5.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +12 -5
- package/dist/discovery/index.d.ts +64 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +195 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/generated-api/index.d.ts +10 -0
- package/dist/generated-api/index.d.ts.map +1 -0
- package/dist/generated-api/index.js +272 -0
- package/dist/generated-api/index.js.map +1 -0
- package/dist/graphql/index.d.ts +3 -3
- package/dist/graphql/index.d.ts.map +1 -1
- package/dist/graphql/index.js +24 -60
- package/dist/graphql/index.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.d.ts +2 -2
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +19 -29
- package/dist/openapi/index.js.map +1 -1
- package/dist/rest/index.d.ts +3 -3
- package/dist/rest/index.d.ts.map +1 -1
- package/dist/rest/index.js +51 -51
- package/dist/rest/index.js.map +1 -1
- package/dist/test-runner/index.d.ts.map +1 -1
- package/dist/test-runner/index.js +60 -104
- package/dist/test-runner/index.js.map +1 -1
- package/package.json +2 -2
- package/src/discovery/index.test.ts +133 -0
- package/src/discovery/index.ts +340 -0
- package/src/{managed-api → generated-api}/index.test.ts +59 -67
- package/src/generated-api/index.ts +410 -0
- package/src/graphql/index.test.ts +28 -134
- package/src/graphql/index.ts +29 -89
- package/src/index.test.ts +25 -18
- package/src/index.ts +29 -7
- package/src/openapi/index.test.ts +36 -246
- package/src/openapi/index.ts +22 -60
- package/src/rest/index.test.ts +22 -173
- package/src/rest/index.ts +62 -59
- package/src/test-runner/index.test.ts +82 -150
- package/src/test-runner/index.ts +73 -143
- package/dist/managed-api/index.d.ts +0 -30
- package/dist/managed-api/index.d.ts.map +0 -1
- package/dist/managed-api/index.js +0 -257
- package/dist/managed-api/index.js.map +0 -1
- package/src/managed-api/index.ts +0 -364
|
@@ -8,289 +8,79 @@ import {
|
|
|
8
8
|
type OpenApiDocumentObject,
|
|
9
9
|
} from './index';
|
|
10
10
|
|
|
11
|
-
function
|
|
12
|
-
expect(JSON.parse(JSON.stringify(value))).toEqual(value);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function createPetStoreDocument(): OpenApiDocumentObject {
|
|
11
|
+
function document(): OpenApiDocumentObject {
|
|
16
12
|
return {
|
|
17
13
|
openapi: '3.1.0',
|
|
18
|
-
info: {
|
|
19
|
-
|
|
20
|
-
version: '2026-05-19',
|
|
21
|
-
description: 'Example API.',
|
|
22
|
-
},
|
|
23
|
-
servers: [
|
|
24
|
-
{
|
|
25
|
-
url: 'https://api.example.com',
|
|
26
|
-
},
|
|
27
|
-
],
|
|
14
|
+
info: { title: 'Pet Store', version: '2026-08-06' },
|
|
15
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
28
16
|
components: {
|
|
29
17
|
schemas: {
|
|
30
18
|
Pet: {
|
|
31
19
|
type: 'object',
|
|
32
|
-
required: ['id'
|
|
33
|
-
properties: {
|
|
34
|
-
id: { type: 'string' },
|
|
35
|
-
name: { type: 'string' },
|
|
36
|
-
age: { type: 'integer' },
|
|
37
|
-
},
|
|
20
|
+
required: ['id'],
|
|
21
|
+
properties: { id: { type: 'string' }, name: { type: 'string' } },
|
|
38
22
|
},
|
|
39
23
|
},
|
|
40
24
|
},
|
|
41
25
|
paths: {
|
|
42
26
|
'/pets': {
|
|
43
|
-
get: {
|
|
44
|
-
|
|
45
|
-
summary: 'List pets',
|
|
46
|
-
parameters: [
|
|
47
|
-
{
|
|
48
|
-
name: 'limit',
|
|
49
|
-
in: 'query',
|
|
50
|
-
schema: { type: 'integer', default: 20 },
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
responses: {
|
|
54
|
-
'200': {
|
|
55
|
-
description: 'Pets',
|
|
56
|
-
content: {
|
|
57
|
-
'application/json': {
|
|
58
|
-
schema: {
|
|
59
|
-
type: 'array',
|
|
60
|
-
items: { $ref: '#/components/schemas/Pet' },
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
post: {
|
|
68
|
-
operationId: 'createPet',
|
|
69
|
-
summary: 'Create pet',
|
|
70
|
-
requestBody: {
|
|
71
|
-
required: true,
|
|
72
|
-
content: {
|
|
73
|
-
'application/json': {
|
|
74
|
-
schema: { $ref: '#/components/schemas/Pet' },
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
responses: {
|
|
79
|
-
'201': {
|
|
80
|
-
description: 'Created pet',
|
|
81
|
-
content: {
|
|
82
|
-
'application/json': {
|
|
83
|
-
schema: { $ref: '#/components/schemas/Pet' },
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
'/pets/{petId}': {
|
|
91
|
-
parameters: [
|
|
92
|
-
{
|
|
93
|
-
name: 'petId',
|
|
94
|
-
in: 'path',
|
|
95
|
-
required: true,
|
|
96
|
-
schema: { type: 'string' },
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
get: {
|
|
100
|
-
operationId: 'getPet',
|
|
101
|
-
responses: {
|
|
102
|
-
'200': {
|
|
103
|
-
description: 'Pet',
|
|
104
|
-
content: {
|
|
105
|
-
'application/json': {
|
|
106
|
-
schema: { $ref: '#/components/schemas/Pet' },
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
patch: {
|
|
113
|
-
operationId: 'updatePet',
|
|
114
|
-
requestBody: {
|
|
115
|
-
content: {
|
|
116
|
-
'application/json': {
|
|
117
|
-
schema: { $ref: '#/components/schemas/Pet' },
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
responses: {
|
|
122
|
-
'200': {
|
|
123
|
-
description: 'Updated pet',
|
|
124
|
-
content: {
|
|
125
|
-
'application/json': {
|
|
126
|
-
schema: { $ref: '#/components/schemas/Pet' },
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
delete: {
|
|
133
|
-
operationId: 'deletePet',
|
|
134
|
-
responses: {
|
|
135
|
-
'204': {
|
|
136
|
-
description: 'Deleted pet',
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
},
|
|
27
|
+
get: { operationId: 'listPets', responses: { '200': { description: 'Pets' } } },
|
|
28
|
+
post: { operationId: 'createPet', responses: { '201': { description: 'Pet' } } },
|
|
140
29
|
},
|
|
141
30
|
},
|
|
142
31
|
};
|
|
143
32
|
}
|
|
144
33
|
|
|
145
|
-
describe('OpenAPI
|
|
146
|
-
it('normalizes
|
|
34
|
+
describe('OpenAPI normalization', () => {
|
|
35
|
+
it('normalizes endpoint, operation and schema shapes', () => {
|
|
147
36
|
expect(normalizeOpenApiEndpointId('/pets/{petId}')).toBe('pets-petid');
|
|
148
37
|
expect(normalizeOpenApiOperationId('get', '/pets/{petId}')).toBe('get-pets-petid');
|
|
149
|
-
expect(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
it('normalizes OpenAPI schemas into data schemas', () => {
|
|
153
|
-
const schema = normalizeOpenApiSchema({
|
|
154
|
-
type: 'object',
|
|
155
|
-
properties: {
|
|
156
|
-
id: { type: 'string' },
|
|
157
|
-
tags: { type: 'array', items: { type: 'string' } },
|
|
158
|
-
},
|
|
159
|
-
required: ['id'],
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
assertSerializable(schema);
|
|
163
|
-
expect(schema.type).toBe('object');
|
|
164
|
-
expect(schema.properties?.tags?.items?.type).toBe('string');
|
|
38
|
+
expect(normalizeOpenApiSchema({ type: 'array', items: { type: 'string' } }).items?.type).toBe(
|
|
39
|
+
'string',
|
|
40
|
+
);
|
|
165
41
|
});
|
|
166
42
|
|
|
167
|
-
it('imports
|
|
43
|
+
it('imports OpenAPI as an external REST source with OpenAPI metadata', () => {
|
|
168
44
|
const result = importOpenApiDocument({
|
|
169
45
|
id: 'pet-store',
|
|
170
|
-
document:
|
|
46
|
+
document: document(),
|
|
171
47
|
documentUrl: 'https://api.example.com/openapi.json',
|
|
172
|
-
credential: {
|
|
173
|
-
id: 'pet-store-token',
|
|
174
|
-
kind: 'bearer',
|
|
175
|
-
},
|
|
176
48
|
});
|
|
177
|
-
|
|
178
49
|
expect(result.ok).toBe(true);
|
|
179
50
|
if (result.ok) {
|
|
180
|
-
|
|
181
|
-
expect(result.data.
|
|
182
|
-
expect(result.data.baseUrl).toBe('https://api.example.com');
|
|
183
|
-
expect(result.data.import?.version).toBe('2026-05-19');
|
|
184
|
-
expect(result.data.schemas?.Pet?.properties?.name?.type).toBe('string');
|
|
51
|
+
expect(result.data).toMatchObject({ kind: 'api', origin: 'external', protocol: 'rest' });
|
|
52
|
+
expect(result.data.openApi?.version).toBe('2026-08-06');
|
|
185
53
|
expect(result.data.endpoints.pets?.operations.listpets?.intent).toBe('read');
|
|
186
54
|
expect(result.data.endpoints.pets?.operations.createpet?.intent).toBe('create');
|
|
187
|
-
expect(
|
|
188
|
-
result.data.endpoints['pets-petid']?.operations.getpet?.request?.parameters?.[0]?.name,
|
|
189
|
-
).toBe('petId');
|
|
190
|
-
expect(result.data.endpoints['pets-petid']?.operations.updatepet?.intent).toBe('update');
|
|
191
|
-
expect(result.data.endpoints['pets-petid']?.operations.deletepet?.intent).toBe('delete');
|
|
192
55
|
}
|
|
193
56
|
});
|
|
194
57
|
|
|
195
|
-
it('uses an explicit
|
|
196
|
-
const document: OpenApiDocumentObject = {
|
|
197
|
-
...createPetStoreDocument(),
|
|
198
|
-
servers: [{ url: 'https://first.example.com' }, { url: 'https://second.example.com' }],
|
|
199
|
-
};
|
|
58
|
+
it('uses an explicit base URL and reports ambiguous servers', () => {
|
|
200
59
|
const result = importOpenApiDocument({
|
|
201
60
|
id: 'pet-store',
|
|
202
|
-
document
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
expect(result.ok).toBe(true);
|
|
207
|
-
if (result.ok) {
|
|
208
|
-
expect(result.data.baseUrl).toBe('https://override.example.com');
|
|
209
|
-
expect(result.diagnostics).toEqual([]);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('reports ambiguous server diagnostics when no baseUrl override exists', () => {
|
|
214
|
-
const document: OpenApiDocumentObject = {
|
|
215
|
-
...createPetStoreDocument(),
|
|
216
|
-
servers: [{ url: 'https://first.example.com' }, { url: 'https://second.example.com' }],
|
|
217
|
-
};
|
|
218
|
-
const result = importOpenApiDocument({ id: 'pet-store', document });
|
|
219
|
-
|
|
220
|
-
expect(result.ok).toBe(true);
|
|
221
|
-
if (result.ok) {
|
|
222
|
-
expect(result.data.baseUrl).toBe('https://first.example.com');
|
|
223
|
-
expect(result.diagnostics?.map((diagnostic) => diagnostic.code)).toContain(
|
|
224
|
-
'ambiguous-server',
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('deduplicates duplicate operation IDs deterministically', () => {
|
|
230
|
-
const document: OpenApiDocumentObject = {
|
|
231
|
-
openapi: '3.1.0',
|
|
232
|
-
paths: {
|
|
233
|
-
'/one': {
|
|
234
|
-
get: { operationId: 'duplicate', responses: { '200': { description: 'OK' } } },
|
|
235
|
-
},
|
|
236
|
-
'/two': {
|
|
237
|
-
get: { operationId: 'duplicate', responses: { '200': { description: 'OK' } } },
|
|
238
|
-
},
|
|
61
|
+
document: {
|
|
62
|
+
...document(),
|
|
63
|
+
servers: [{ url: 'https://one.example.com' }, { url: 'https://two.example.com' }],
|
|
239
64
|
},
|
|
240
|
-
|
|
241
|
-
const result = importOpenApiDocument({
|
|
242
|
-
id: 'duplicates',
|
|
243
|
-
document,
|
|
244
|
-
baseUrl: 'https://api.example.com',
|
|
65
|
+
baseUrl: 'https://override.example.com',
|
|
245
66
|
});
|
|
246
|
-
|
|
247
67
|
expect(result.ok).toBe(true);
|
|
248
|
-
if (result.ok)
|
|
249
|
-
expect(result.data.endpoints.one?.operations.duplicate?.id).toBe('duplicate');
|
|
250
|
-
expect(result.data.endpoints.two?.operations['duplicate-2']?.id).toBe('duplicate-2');
|
|
251
|
-
expect(result.diagnostics?.map((diagnostic) => diagnostic.code)).toContain(
|
|
252
|
-
'duplicate-operation-id',
|
|
253
|
-
);
|
|
254
|
-
}
|
|
68
|
+
if (result.ok) expect(result.data.baseUrl).toBe('https://override.example.com');
|
|
255
69
|
});
|
|
256
70
|
|
|
257
|
-
it('
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
expect(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const result = importOpenApiDocument({
|
|
272
|
-
id: 'unsupported-schema',
|
|
273
|
-
baseUrl: 'https://api.example.com',
|
|
274
|
-
document: {
|
|
275
|
-
openapi: '3.1.0',
|
|
276
|
-
components: {
|
|
277
|
-
schemas: {
|
|
278
|
-
Weird: { type: 'file' },
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
|
-
paths: {
|
|
282
|
-
'/weird': {
|
|
283
|
-
get: { responses: { '200': { description: 'OK' } } },
|
|
284
|
-
},
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
expect(result.ok).toBe(true);
|
|
290
|
-
if (result.ok) {
|
|
291
|
-
expect(result.diagnostics?.map((diagnostic) => diagnostic.code)).toContain(
|
|
292
|
-
'unsupported-schema',
|
|
293
|
-
);
|
|
294
|
-
}
|
|
71
|
+
it('requires a resolvable base URL and paths', () => {
|
|
72
|
+
expect(
|
|
73
|
+
importOpenApiDocument({
|
|
74
|
+
id: 'no-server',
|
|
75
|
+
document: { openapi: '3.1.0', paths: { '/x': { get: {} } } },
|
|
76
|
+
}).ok,
|
|
77
|
+
).toBe(false);
|
|
78
|
+
expect(
|
|
79
|
+
importOpenApiDocument({
|
|
80
|
+
id: 'no-paths',
|
|
81
|
+
baseUrl: 'https://example.com',
|
|
82
|
+
document: { openapi: '3.1.0' },
|
|
83
|
+
}).ok,
|
|
84
|
+
).toBe(false);
|
|
295
85
|
});
|
|
296
86
|
});
|
package/src/openapi/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
DataSourceDiagnostic,
|
|
15
15
|
DataSourceDiagnosticResult,
|
|
16
16
|
EndpointId,
|
|
17
|
-
|
|
17
|
+
ExternalRestApiDataSourceConfig,
|
|
18
18
|
OperationId,
|
|
19
19
|
} from '@ankhorage/contracts/data';
|
|
20
20
|
|
|
@@ -121,7 +121,7 @@ export interface OpenApiImportInput {
|
|
|
121
121
|
readonly metadata?: DataContractValue;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
export type OpenApiImportResult = DataSourceDiagnosticResult<
|
|
124
|
+
export type OpenApiImportResult = DataSourceDiagnosticResult<ExternalRestApiDataSourceConfig>;
|
|
125
125
|
|
|
126
126
|
export function importOpenApiDocument(input: OpenApiImportInput): OpenApiImportResult {
|
|
127
127
|
const diagnostics: DataSourceDiagnostic[] = [];
|
|
@@ -132,9 +132,8 @@ export function importOpenApiDocument(input: OpenApiImportInput): OpenApiImportR
|
|
|
132
132
|
diagnostics,
|
|
133
133
|
);
|
|
134
134
|
const endpoints = normalizeOpenApiEndpoints(input, baseUrl, diagnostics);
|
|
135
|
-
const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === 'error');
|
|
136
135
|
|
|
137
|
-
if (
|
|
136
|
+
if (baseUrl === undefined || diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
|
138
137
|
return { ok: false, diagnostics };
|
|
139
138
|
}
|
|
140
139
|
|
|
@@ -142,14 +141,16 @@ export function importOpenApiDocument(input: OpenApiImportInput): OpenApiImportR
|
|
|
142
141
|
ok: true,
|
|
143
142
|
data: {
|
|
144
143
|
id: input.id,
|
|
145
|
-
kind: '
|
|
144
|
+
kind: 'api',
|
|
145
|
+
origin: 'external',
|
|
146
|
+
protocol: 'rest',
|
|
146
147
|
name: input.name ?? input.document.info?.title,
|
|
147
148
|
description: input.description ?? input.document.info?.description,
|
|
148
149
|
baseUrl,
|
|
149
150
|
credential: input.credential,
|
|
150
151
|
endpoints,
|
|
151
152
|
schemas,
|
|
152
|
-
|
|
153
|
+
openApi: {
|
|
153
154
|
url: input.documentUrl,
|
|
154
155
|
documentId: input.documentId,
|
|
155
156
|
version: input.document.info?.version,
|
|
@@ -171,7 +172,6 @@ export function normalizeOpenApiOperationId(
|
|
|
171
172
|
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
172
173
|
.replace(/^-+|-+$/g, '')
|
|
173
174
|
.toLowerCase();
|
|
174
|
-
|
|
175
175
|
return normalized.length > 0 ? normalized : `${method}-root`;
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -181,13 +181,10 @@ export function normalizeOpenApiEndpointId(path: string): EndpointId {
|
|
|
181
181
|
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
182
182
|
.replace(/^-+|-+$/g, '')
|
|
183
183
|
.toLowerCase();
|
|
184
|
-
|
|
185
184
|
return normalized.length > 0 ? normalized : 'root';
|
|
186
185
|
}
|
|
187
186
|
|
|
188
187
|
export function normalizeOpenApiSchema(schema: OpenApiSchemaObject): DataSchema {
|
|
189
|
-
const additionalProperties = normalizeAdditionalProperties(schema.additionalProperties);
|
|
190
|
-
|
|
191
188
|
return {
|
|
192
189
|
ref: normalizeOpenApiSchemaRef(schema.$ref),
|
|
193
190
|
type: normalizeSchemaType(schema.type),
|
|
@@ -200,7 +197,7 @@ export function normalizeOpenApiSchema(schema: OpenApiSchemaObject): DataSchema
|
|
|
200
197
|
nullable: schema.nullable,
|
|
201
198
|
required: schema.required,
|
|
202
199
|
properties: normalizeSchemaRecord(schema.properties),
|
|
203
|
-
additionalProperties,
|
|
200
|
+
additionalProperties: normalizeAdditionalProperties(schema.additionalProperties),
|
|
204
201
|
items: schema.items === undefined ? undefined : normalizeOpenApiSchema(schema.items),
|
|
205
202
|
allOf: normalizeSchemaList(schema.allOf),
|
|
206
203
|
anyOf: normalizeSchemaList(schema.anyOf),
|
|
@@ -212,18 +209,16 @@ function resolveOpenApiBaseUrl(
|
|
|
212
209
|
input: OpenApiImportInput,
|
|
213
210
|
diagnostics: DataSourceDiagnostic[],
|
|
214
211
|
): string | undefined {
|
|
215
|
-
if (input.baseUrl !== undefined) return input.baseUrl;
|
|
212
|
+
if (input.baseUrl !== undefined && input.baseUrl.trim().length > 0) return input.baseUrl;
|
|
216
213
|
|
|
217
214
|
const servers = input.document.servers ?? [];
|
|
218
|
-
|
|
219
215
|
if (servers.length === 0) {
|
|
220
216
|
diagnostics.push({
|
|
221
217
|
code: 'ambiguous-server',
|
|
222
218
|
dataSourceId: input.id,
|
|
223
|
-
message:
|
|
224
|
-
'OpenAPI document does not define a server URL and no baseUrl override was provided.',
|
|
219
|
+
message: 'OpenAPI import requires a server URL or an explicit baseUrl override.',
|
|
225
220
|
path: 'servers',
|
|
226
|
-
severity: '
|
|
221
|
+
severity: 'error',
|
|
227
222
|
});
|
|
228
223
|
return undefined;
|
|
229
224
|
}
|
|
@@ -247,7 +242,6 @@ function normalizeOpenApiSchemas(
|
|
|
247
242
|
diagnostics: DataSourceDiagnostic[],
|
|
248
243
|
): DataSchemaRegistry | undefined {
|
|
249
244
|
if (schemas === undefined) return undefined;
|
|
250
|
-
|
|
251
245
|
const registry: Record<string, DataSchema> = {};
|
|
252
246
|
|
|
253
247
|
for (const [schemaId, schema] of Object.entries(schemas)) {
|
|
@@ -259,7 +253,6 @@ function normalizeOpenApiSchemas(
|
|
|
259
253
|
diagnostics,
|
|
260
254
|
);
|
|
261
255
|
}
|
|
262
|
-
|
|
263
256
|
return registry;
|
|
264
257
|
}
|
|
265
258
|
|
|
@@ -269,7 +262,6 @@ function normalizeOpenApiEndpoints(
|
|
|
269
262
|
diagnostics: DataSourceDiagnostic[],
|
|
270
263
|
): Record<EndpointId, DataEndpointConfig> {
|
|
271
264
|
const { paths } = input.document;
|
|
272
|
-
|
|
273
265
|
if (paths === undefined || Object.keys(paths).length === 0) {
|
|
274
266
|
diagnostics.push({
|
|
275
267
|
code: 'invalid-config',
|
|
@@ -308,8 +300,6 @@ function normalizeOpenApiEndpoints(
|
|
|
308
300
|
operationId,
|
|
309
301
|
diagnostics,
|
|
310
302
|
);
|
|
311
|
-
const request = normalizeOpenApiRequest(operation.requestBody, parameters);
|
|
312
|
-
const response = normalizeOpenApiResponse(operation.responses);
|
|
313
303
|
|
|
314
304
|
operations[operationId] = {
|
|
315
305
|
id: operationId,
|
|
@@ -320,8 +310,8 @@ function normalizeOpenApiEndpoints(
|
|
|
320
310
|
intent: mapOpenApiMethodToIntent(method),
|
|
321
311
|
method: method.toUpperCase(),
|
|
322
312
|
path,
|
|
323
|
-
request,
|
|
324
|
-
response,
|
|
313
|
+
request: normalizeOpenApiRequest(operation.requestBody, parameters),
|
|
314
|
+
response: normalizeOpenApiResponse(operation.responses),
|
|
325
315
|
metadata: {
|
|
326
316
|
deprecated: operation.deprecated ?? false,
|
|
327
317
|
source: 'openapi',
|
|
@@ -352,7 +342,6 @@ function resolveUniqueOperationId(
|
|
|
352
342
|
diagnostics: DataSourceDiagnostic[],
|
|
353
343
|
): OperationId {
|
|
354
344
|
const normalized = normalizeOpenApiOperationId(method, path, operationId);
|
|
355
|
-
|
|
356
345
|
if (!operationIds.has(normalized)) {
|
|
357
346
|
operationIds.add(normalized);
|
|
358
347
|
return normalized;
|
|
@@ -360,7 +349,6 @@ function resolveUniqueOperationId(
|
|
|
360
349
|
|
|
361
350
|
let suffix = 2;
|
|
362
351
|
let uniqueId: OperationId = `${normalized}-${suffix}`;
|
|
363
|
-
|
|
364
352
|
while (operationIds.has(uniqueId)) {
|
|
365
353
|
suffix += 1;
|
|
366
354
|
uniqueId = `${normalized}-${suffix}`;
|
|
@@ -376,7 +364,6 @@ function resolveUniqueOperationId(
|
|
|
376
364
|
path: `paths.${path}.${method}.operationId`,
|
|
377
365
|
severity: 'warning',
|
|
378
366
|
});
|
|
379
|
-
|
|
380
367
|
return uniqueId;
|
|
381
368
|
}
|
|
382
369
|
|
|
@@ -388,12 +375,10 @@ function normalizeOpenApiParameters(
|
|
|
388
375
|
diagnostics: DataSourceDiagnostic[],
|
|
389
376
|
): readonly DataOperationParameter[] | undefined {
|
|
390
377
|
if (parameters.length === 0) return undefined;
|
|
391
|
-
|
|
392
378
|
const normalized: DataOperationParameter[] = [];
|
|
393
379
|
|
|
394
380
|
for (const parameter of parameters) {
|
|
395
381
|
const location = normalizeParameterLocation(parameter.in);
|
|
396
|
-
|
|
397
382
|
if (location === undefined) {
|
|
398
383
|
diagnostics.push({
|
|
399
384
|
code: 'invalid-config',
|
|
@@ -428,7 +413,6 @@ function normalizeParameterLocation(location: string): DataOperationParameterLoc
|
|
|
428
413
|
) {
|
|
429
414
|
return location;
|
|
430
415
|
}
|
|
431
|
-
|
|
432
416
|
return undefined;
|
|
433
417
|
}
|
|
434
418
|
|
|
@@ -437,9 +421,7 @@ function normalizeOpenApiRequest(
|
|
|
437
421
|
parameters: readonly DataOperationParameter[] | undefined,
|
|
438
422
|
): DataOperationRequest | undefined {
|
|
439
423
|
const content = firstContentEntry(requestBody?.content);
|
|
440
|
-
|
|
441
424
|
if (requestBody === undefined && parameters === undefined) return undefined;
|
|
442
|
-
|
|
443
425
|
return {
|
|
444
426
|
contentType: content?.contentType,
|
|
445
427
|
parameters,
|
|
@@ -451,13 +433,11 @@ function normalizeOpenApiResponse(
|
|
|
451
433
|
responses: Readonly<Record<string, OpenApiResponseObject>> | undefined,
|
|
452
434
|
): DataOperationResponse | undefined {
|
|
453
435
|
if (responses === undefined) return undefined;
|
|
454
|
-
|
|
455
436
|
const status = chooseResponseStatus(Object.keys(responses));
|
|
456
437
|
if (status === undefined) return undefined;
|
|
457
438
|
|
|
458
439
|
const response = responses[status];
|
|
459
440
|
const content = firstContentEntry(response?.content);
|
|
460
|
-
|
|
461
441
|
return {
|
|
462
442
|
status,
|
|
463
443
|
contentType: content?.contentType,
|
|
@@ -470,10 +450,8 @@ function firstContentEntry(
|
|
|
470
450
|
content: Readonly<Record<string, OpenApiMediaTypeObject>> | undefined,
|
|
471
451
|
): { readonly contentType: string; readonly schema?: OpenApiSchemaObject } | undefined {
|
|
472
452
|
if (content === undefined) return undefined;
|
|
473
|
-
|
|
474
453
|
const [entry] = Object.entries(content);
|
|
475
454
|
if (entry === undefined) return undefined;
|
|
476
|
-
|
|
477
455
|
const [contentType, media] = entry;
|
|
478
456
|
return { contentType, schema: media.schema };
|
|
479
457
|
}
|
|
@@ -492,52 +470,37 @@ function mapOpenApiMethodToIntent(method: OpenApiHttpMethod): DataOperationInten
|
|
|
492
470
|
|
|
493
471
|
function normalizeOpenApiSchemaRef(ref: string | undefined): { readonly id: string } | undefined {
|
|
494
472
|
if (ref === undefined) return undefined;
|
|
495
|
-
|
|
496
473
|
const prefix = '#/components/schemas/';
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return { id: ref };
|
|
474
|
+
return { id: ref.startsWith(prefix) ? ref.slice(prefix.length) : ref };
|
|
500
475
|
}
|
|
501
476
|
|
|
502
477
|
function normalizeSchemaRecord(
|
|
503
478
|
record: Readonly<Record<string, OpenApiSchemaObject>> | undefined,
|
|
504
479
|
): Readonly<Record<string, DataSchema>> | undefined {
|
|
505
480
|
if (record === undefined) return undefined;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
for (const [key, schema] of Object.entries(record)) {
|
|
510
|
-
normalized[key] = normalizeOpenApiSchema(schema);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return normalized;
|
|
481
|
+
return Object.fromEntries(
|
|
482
|
+
Object.entries(record).map(([key, schema]) => [key, normalizeOpenApiSchema(schema)]),
|
|
483
|
+
);
|
|
514
484
|
}
|
|
515
485
|
|
|
516
486
|
function normalizeSchemaList(
|
|
517
487
|
list: readonly OpenApiSchemaObject[] | undefined,
|
|
518
488
|
): readonly DataSchema[] | undefined {
|
|
519
|
-
return list?.map(
|
|
489
|
+
return list?.map(normalizeOpenApiSchema);
|
|
520
490
|
}
|
|
521
491
|
|
|
522
492
|
function normalizeAdditionalProperties(
|
|
523
|
-
|
|
493
|
+
value: boolean | OpenApiSchemaObject | undefined,
|
|
524
494
|
): boolean | DataSchema | undefined {
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return normalizeOpenApiSchema(additionalProperties);
|
|
495
|
+
if (value === undefined || typeof value === 'boolean') return value;
|
|
496
|
+
return normalizeOpenApiSchema(value);
|
|
530
497
|
}
|
|
531
498
|
|
|
532
499
|
function normalizeSchemaType(
|
|
533
500
|
type: string | readonly string[] | undefined,
|
|
534
501
|
): DataSchemaPrimitiveType | readonly DataSchemaPrimitiveType[] | undefined {
|
|
535
502
|
if (type === undefined) return undefined;
|
|
536
|
-
|
|
537
|
-
if (typeof type === 'string') {
|
|
538
|
-
return isDataSchemaPrimitive(type) ? type : undefined;
|
|
539
|
-
}
|
|
540
|
-
|
|
503
|
+
if (typeof type === 'string') return isDataSchemaPrimitive(type) ? type : undefined;
|
|
541
504
|
return type.filter(isDataSchemaPrimitive);
|
|
542
505
|
}
|
|
543
506
|
|
|
@@ -569,7 +532,6 @@ function collectUnsupportedSchemaDiagnostics(
|
|
|
569
532
|
diagnostics,
|
|
570
533
|
);
|
|
571
534
|
}
|
|
572
|
-
|
|
573
535
|
if (schema.items !== undefined) {
|
|
574
536
|
collectUnsupportedSchemaDiagnostics(schema.items, dataSourceId, `${path}.items`, diagnostics);
|
|
575
537
|
}
|