@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
@@ -3,10 +3,12 @@ import { describe, expect, it } from 'bun:test';
3
3
 
4
4
  import { buildEndpointTestRequest, type EndpointTestFetch, testEndpoint } from './index';
5
5
 
6
- function createRestDataSource(): DataSourceConfig {
6
+ function restSource(): DataSourceConfig {
7
7
  return {
8
8
  id: 'cms',
9
- kind: 'rest',
9
+ kind: 'api',
10
+ origin: 'external',
11
+ protocol: 'rest',
10
12
  baseUrl: 'https://cms.example.com',
11
13
  credential: { id: 'cms-token', kind: 'bearer' },
12
14
  endpoints: {
@@ -30,19 +32,9 @@ function createRestDataSource(): DataSourceConfig {
30
32
  required: true,
31
33
  schema: { type: 'string' },
32
34
  },
33
- {
34
- name: 'preview',
35
- location: 'query',
36
- schema: { type: 'boolean' },
37
- },
35
+ { name: 'preview', location: 'query', schema: { type: 'boolean' } },
38
36
  ],
39
37
  },
40
- response: {
41
- status: 200,
42
- schema: {
43
- type: 'object',
44
- },
45
- },
46
38
  },
47
39
  'posts.create': {
48
40
  id: 'posts.create',
@@ -51,14 +43,6 @@ function createRestDataSource(): DataSourceConfig {
51
43
  intent: 'create',
52
44
  method: 'POST',
53
45
  path: '/posts',
54
- request: {
55
- contentType: 'application/json',
56
- schema: { type: 'object' },
57
- },
58
- response: {
59
- status: 201,
60
- schema: { type: 'object' },
61
- },
62
46
  },
63
47
  },
64
48
  },
@@ -66,10 +50,12 @@ function createRestDataSource(): DataSourceConfig {
66
50
  };
67
51
  }
68
52
 
69
- function createGraphQlDataSource(): DataSourceConfig {
53
+ function graphQlSource(): DataSourceConfig {
70
54
  return {
71
55
  id: 'content',
72
- kind: 'graphql',
56
+ kind: 'api',
57
+ origin: 'external',
58
+ protocol: 'graphql',
73
59
  endpointUrl: 'https://content.example.com/graphql',
74
60
  endpoints: {
75
61
  graphql: {
@@ -81,12 +67,7 @@ function createGraphQlDataSource(): DataSourceConfig {
81
67
  endpointId: 'graphql',
82
68
  protocol: 'graphql',
83
69
  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
- },
70
+ metadata: { document: 'query Viewer($id: ID!) { viewer(id: $id) { id } }' },
90
71
  },
91
72
  },
92
73
  },
@@ -95,172 +76,123 @@ function createGraphQlDataSource(): DataSourceConfig {
95
76
  }
96
77
 
97
78
  describe('endpoint test runner', () => {
98
- it('builds a dry-run REST request with path, query, and credential headers', async () => {
79
+ it('builds REST dry runs with params and credential headers', async () => {
99
80
  const result = await buildEndpointTestRequest({
100
- dataSource: createRestDataSource(),
81
+ dataSource: restSource(),
101
82
  endpointId: 'posts',
102
83
  operationId: 'posts.get',
103
84
  dryRun: true,
104
- values: {
105
- postId: 'post 1',
106
- preview: true,
107
- },
108
- credentialResolver: () => ({
109
- headers: { authorization: 'Bearer token' },
110
- }),
85
+ values: { postId: 'post 1', preview: true },
86
+ credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
111
87
  });
112
-
113
88
  expect(result.ok).toBe(true);
114
89
  if (result.ok) {
115
90
  expect(result.request.url).toBe('https://cms.example.com/posts/post%201?preview=true');
116
- expect(result.request.method).toBe('GET');
117
91
  expect(result.request.headers.authorization).toBe('Bearer token');
118
- expect(result.request.dryRun).toBe(true);
119
92
  }
120
93
  });
121
94
 
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
-
95
+ it('executes REST requests and parses responses', async () => {
96
+ const fetch: EndpointTestFetch = () =>
97
+ Promise.resolve({ status: 200, text: () => Promise.resolve('{"id":"post-1"}') });
133
98
  const result = await testEndpoint({
134
- dataSource: createRestDataSource(),
99
+ dataSource: restSource(),
135
100
  endpointId: 'posts',
136
101
  operationId: 'posts.get',
137
102
  values: { postId: 'post-1' },
138
103
  credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
139
104
  fetch,
140
105
  });
141
-
142
106
  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
- }
107
+ if (result.ok) expect(result.data).toEqual({ id: 'post-1' });
147
108
  });
148
109
 
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(),
110
+ it('returns HTTP, network, credential and input diagnostics', async () => {
111
+ const http = await testEndpoint({
112
+ dataSource: restSource(),
158
113
  endpointId: 'posts',
159
114
  operationId: 'posts.get',
160
115
  values: { postId: 'missing' },
161
- credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
162
- fetch,
116
+ credentialResolver: () => ({ headers: {} }),
117
+ fetch: () => Promise.resolve({ status: 404, text: () => Promise.resolve('{}') }),
163
118
  });
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(),
119
+ const network = await testEndpoint({
120
+ dataSource: restSource(),
177
121
  endpointId: 'posts',
178
122
  operationId: 'posts.get',
179
- values: { postId: 'post-1' },
180
- credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
181
- fetch,
123
+ values: { postId: 'x' },
124
+ credentialResolver: () => ({ headers: {} }),
125
+ fetch: () => Promise.reject(new Error('Connection failed')),
182
126
  });
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(),
127
+ const credential = await buildEndpointTestRequest({
128
+ dataSource: restSource(),
196
129
  endpointId: 'posts',
197
130
  operationId: 'posts.get',
198
- values: { postId: 'post-1' },
131
+ values: { postId: 'x' },
199
132
  });
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(),
133
+ const input = await buildEndpointTestRequest({
134
+ dataSource: restSource(),
212
135
  endpointId: 'posts',
213
136
  operationId: 'posts.get',
214
- credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
137
+ credentialResolver: () => ({ headers: {} }),
215
138
  });
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
- }
139
+ expect(http.ok).toBe(false);
140
+ expect(network.ok).toBe(false);
141
+ expect(credential.ok).toBe(false);
142
+ expect(input.ok).toBe(false);
223
143
  });
224
144
 
225
- it('builds POST request bodies from input body values', async () => {
226
- const result = await buildEndpointTestRequest({
227
- dataSource: createRestDataSource(),
145
+ it('builds POST bodies and GraphQL requests', async () => {
146
+ const post = await buildEndpointTestRequest({
147
+ dataSource: restSource(),
228
148
  endpointId: 'posts',
229
149
  operationId: 'posts.create',
230
150
  dryRun: true,
231
- values: {
232
- body: { title: 'Hello' },
233
- },
234
- credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
151
+ values: { body: { title: 'Hello' } },
152
+ credentialResolver: () => ({ headers: {} }),
235
153
  });
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(),
154
+ const graphql = await buildEndpointTestRequest({
155
+ dataSource: graphQlSource(),
248
156
  endpointId: 'graphql',
249
157
  operationId: 'query.viewer',
250
158
  dryRun: true,
251
- values: {
252
- variables: { id: 'viewer-1' },
253
- },
159
+ values: { variables: { id: 'viewer-1' } },
254
160
  });
161
+ expect(post.ok).toBe(true);
162
+ if (post.ok) expect(post.request.body).toBe('{"title":"Hello"}');
163
+ expect(graphql.ok).toBe(true);
164
+ if (graphql.ok) expect(graphql.request.url).toBe('https://content.example.com/graphql');
165
+ });
255
166
 
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
- }
167
+ it('does not pretend database-backed generated operations are HTTP endpoints', async () => {
168
+ const source: DataSourceConfig = {
169
+ id: 'catalog',
170
+ kind: 'api',
171
+ origin: 'generated',
172
+ protocol: 'rest',
173
+ generatedApiId: 'catalog',
174
+ adapter: { id: 'db', kind: 'database' },
175
+ endpoints: {
176
+ products: {
177
+ id: 'products',
178
+ kind: 'database',
179
+ operations: {
180
+ 'products.list': {
181
+ id: 'products.list',
182
+ endpointId: 'products',
183
+ protocol: 'database',
184
+ intent: 'read',
185
+ },
186
+ },
187
+ },
188
+ },
189
+ };
190
+ const result = await buildEndpointTestRequest({
191
+ dataSource: source,
192
+ endpointId: 'products',
193
+ operationId: 'products.list',
194
+ });
195
+ expect(result.ok).toBe(false);
196
+ if (!result.ok) expect(result.diagnostics[0]?.message).toContain('adapter executor');
265
197
  });
266
198
  });