@ankhorage/data-sources 0.6.0 → 1.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -5
  3. package/dist/discovery/index.d.ts +3 -3
  4. package/dist/discovery/index.d.ts.map +1 -1
  5. package/dist/discovery/index.js +19 -28
  6. package/dist/discovery/index.js.map +1 -1
  7. package/dist/generated-api/index.d.ts +10 -0
  8. package/dist/generated-api/index.d.ts.map +1 -0
  9. package/dist/generated-api/index.js +274 -0
  10. package/dist/generated-api/index.js.map +1 -0
  11. package/dist/generated-api/validateGeneratedApiCollection.d.ts +3 -0
  12. package/dist/generated-api/validateGeneratedApiCollection.d.ts.map +1 -0
  13. package/dist/generated-api/validateGeneratedApiCollection.js +35 -0
  14. package/dist/generated-api/validateGeneratedApiCollection.js.map +1 -0
  15. package/dist/graphql/index.d.ts +3 -3
  16. package/dist/graphql/index.d.ts.map +1 -1
  17. package/dist/graphql/index.js +24 -60
  18. package/dist/graphql/index.js.map +1 -1
  19. package/dist/index.d.ts +9 -3
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +17 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/openapi/index.d.ts +2 -2
  24. package/dist/openapi/index.d.ts.map +1 -1
  25. package/dist/openapi/index.js +19 -29
  26. package/dist/openapi/index.js.map +1 -1
  27. package/dist/rest/index.d.ts +3 -3
  28. package/dist/rest/index.d.ts.map +1 -1
  29. package/dist/rest/index.js +51 -51
  30. package/dist/rest/index.js.map +1 -1
  31. package/dist/test-runner/index.d.ts.map +1 -1
  32. package/dist/test-runner/index.js +60 -104
  33. package/dist/test-runner/index.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/discovery/index.test.ts +25 -101
  36. package/src/discovery/index.ts +22 -35
  37. package/src/{managed-api → generated-api}/index.test.ts +59 -67
  38. package/src/generated-api/index.ts +414 -0
  39. package/src/generated-api/validateGeneratedApiCollection.test.ts +91 -0
  40. package/src/generated-api/validateGeneratedApiCollection.ts +76 -0
  41. package/src/graphql/index.test.ts +28 -134
  42. package/src/graphql/index.ts +29 -89
  43. package/src/index.test.ts +25 -18
  44. package/src/index.ts +28 -7
  45. package/src/openapi/index.test.ts +36 -246
  46. package/src/openapi/index.ts +22 -60
  47. package/src/rest/index.test.ts +22 -173
  48. package/src/rest/index.ts +62 -59
  49. package/src/test-runner/index.test.ts +82 -150
  50. package/src/test-runner/index.ts +73 -143
  51. package/dist/managed-api/index.d.ts +0 -30
  52. package/dist/managed-api/index.d.ts.map +0 -1
  53. package/dist/managed-api/index.js +0 -257
  54. package/dist/managed-api/index.js.map +0 -1
  55. 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 assertSerializable<TValue>(value: TValue): void {
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
- title: 'Pet Store',
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', 'name'],
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
- operationId: 'listPets',
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 import normalization', () => {
146
- it('normalizes deterministic endpoint and operation ids', () => {
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(normalizeOpenApiOperationId('get', '/', '')).toBe('get-root');
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 a minimal OpenAPI document into a normalized OpenAPI data source', () => {
43
+ it('imports OpenAPI as an external REST source with OpenAPI metadata', () => {
168
44
  const result = importOpenApiDocument({
169
45
  id: 'pet-store',
170
- document: createPetStoreDocument(),
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
- assertSerializable(result.data);
181
- expect(result.data.kind).toBe('openapi');
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 baseUrl override and reports ambiguous servers', () => {
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
- baseUrl: 'https://override.example.com',
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('returns an error diagnostic when paths are missing', () => {
258
- const result = importOpenApiDocument({
259
- id: 'empty',
260
- document: { openapi: '3.1.0' },
261
- baseUrl: 'https://api.example.com',
262
- });
263
-
264
- expect(result.ok).toBe(false);
265
- if (!result.ok) {
266
- expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toContain('invalid-config');
267
- }
268
- });
269
-
270
- it('reports unsupported schema types as warnings', () => {
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
  });
@@ -14,7 +14,7 @@ import type {
14
14
  DataSourceDiagnostic,
15
15
  DataSourceDiagnosticResult,
16
16
  EndpointId,
17
- OpenApiDataSourceConfig,
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<OpenApiDataSourceConfig>;
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 (hasErrors) {
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: 'openapi',
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
- import: {
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: 'warning',
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
- if (ref.startsWith(prefix)) return { id: ref.slice(prefix.length) };
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
- const normalized: Record<string, DataSchema> = {};
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((schema) => normalizeOpenApiSchema(schema));
489
+ return list?.map(normalizeOpenApiSchema);
520
490
  }
521
491
 
522
492
  function normalizeAdditionalProperties(
523
- additionalProperties: boolean | OpenApiSchemaObject | undefined,
493
+ value: boolean | OpenApiSchemaObject | undefined,
524
494
  ): boolean | DataSchema | undefined {
525
- if (additionalProperties === undefined || typeof additionalProperties === 'boolean') {
526
- return additionalProperties;
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
  }