@ankhorage/data-sources 0.0.0 → 0.2.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.
@@ -0,0 +1,296 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import {
4
+ importOpenApiDocument,
5
+ normalizeOpenApiEndpointId,
6
+ normalizeOpenApiOperationId,
7
+ normalizeOpenApiSchema,
8
+ type OpenApiDocumentObject,
9
+ } from './index';
10
+
11
+ function assertSerializable<TValue>(value: TValue): void {
12
+ expect(JSON.parse(JSON.stringify(value))).toEqual(value);
13
+ }
14
+
15
+ function createPetStoreDocument(): OpenApiDocumentObject {
16
+ return {
17
+ 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
+ ],
28
+ components: {
29
+ schemas: {
30
+ Pet: {
31
+ type: 'object',
32
+ required: ['id', 'name'],
33
+ properties: {
34
+ id: { type: 'string' },
35
+ name: { type: 'string' },
36
+ age: { type: 'integer' },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ paths: {
42
+ '/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
+ },
140
+ },
141
+ },
142
+ };
143
+ }
144
+
145
+ describe('OpenAPI import normalization', () => {
146
+ it('normalizes deterministic endpoint and operation ids', () => {
147
+ expect(normalizeOpenApiEndpointId('/pets/{petId}')).toBe('pets-petid');
148
+ 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');
165
+ });
166
+
167
+ it('imports a minimal OpenAPI document into a normalized OpenAPI data source', () => {
168
+ const result = importOpenApiDocument({
169
+ id: 'pet-store',
170
+ document: createPetStoreDocument(),
171
+ documentUrl: 'https://api.example.com/openapi.json',
172
+ credential: {
173
+ id: 'pet-store-token',
174
+ kind: 'bearer',
175
+ },
176
+ });
177
+
178
+ expect(result.ok).toBe(true);
179
+ 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');
185
+ expect(result.data.endpoints.pets?.operations.listpets?.intent).toBe('read');
186
+ 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
+ }
193
+ });
194
+
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
+ };
200
+ const result = importOpenApiDocument({
201
+ 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
+ },
239
+ },
240
+ };
241
+ const result = importOpenApiDocument({
242
+ id: 'duplicates',
243
+ document,
244
+ baseUrl: 'https://api.example.com',
245
+ });
246
+
247
+ 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
+ }
255
+ });
256
+
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
+ }
295
+ });
296
+ });