@ankhorage/data-sources 0.6.0 → 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.
Files changed (49) hide show
  1. package/CHANGELOG.md +6 -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 +272 -0
  10. package/dist/generated-api/index.js.map +1 -0
  11. package/dist/graphql/index.d.ts +3 -3
  12. package/dist/graphql/index.d.ts.map +1 -1
  13. package/dist/graphql/index.js +24 -60
  14. package/dist/graphql/index.js.map +1 -1
  15. package/dist/index.d.ts +9 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +17 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/openapi/index.d.ts +2 -2
  20. package/dist/openapi/index.d.ts.map +1 -1
  21. package/dist/openapi/index.js +19 -29
  22. package/dist/openapi/index.js.map +1 -1
  23. package/dist/rest/index.d.ts +3 -3
  24. package/dist/rest/index.d.ts.map +1 -1
  25. package/dist/rest/index.js +51 -51
  26. package/dist/rest/index.js.map +1 -1
  27. package/dist/test-runner/index.d.ts.map +1 -1
  28. package/dist/test-runner/index.js +60 -104
  29. package/dist/test-runner/index.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/discovery/index.test.ts +25 -101
  32. package/src/discovery/index.ts +22 -35
  33. package/src/{managed-api → generated-api}/index.test.ts +59 -67
  34. package/src/generated-api/index.ts +410 -0
  35. package/src/graphql/index.test.ts +28 -134
  36. package/src/graphql/index.ts +29 -89
  37. package/src/index.test.ts +25 -18
  38. package/src/index.ts +28 -7
  39. package/src/openapi/index.test.ts +36 -246
  40. package/src/openapi/index.ts +22 -60
  41. package/src/rest/index.test.ts +22 -173
  42. package/src/rest/index.ts +62 -59
  43. package/src/test-runner/index.test.ts +82 -150
  44. package/src/test-runner/index.ts +73 -143
  45. package/dist/managed-api/index.d.ts +0 -30
  46. package/dist/managed-api/index.d.ts.map +0 -1
  47. package/dist/managed-api/index.js +0 -257
  48. package/dist/managed-api/index.js.map +0 -1
  49. package/src/managed-api/index.ts +0 -364
@@ -9,77 +9,13 @@ import {
9
9
  normalizeManualRestMethod,
10
10
  } from './index';
11
11
 
12
- function assertSerializable<TValue>(value: TValue): void {
13
- expect(JSON.parse(JSON.stringify(value))).toEqual(value);
14
- }
15
-
16
- function createBlogDefinition(): ManualRestDataSourceDefinition {
12
+ function definition(): ManualRestDataSourceDefinition {
17
13
  return {
18
14
  id: 'blog-api',
19
- name: 'Blog API',
20
15
  baseUrl: 'https://api.example.com',
21
- credential: {
22
- id: 'blog-api-token',
23
- kind: 'bearer',
24
- label: 'Blog API token',
25
- },
26
- schemas: {
27
- post: {
28
- type: 'object',
29
- required: ['id', 'title'],
30
- properties: {
31
- id: { type: 'string' },
32
- title: { type: 'string' },
33
- published: { type: 'boolean' },
34
- },
35
- },
36
- },
37
16
  endpoints: [
38
17
  {
39
18
  id: 'posts',
40
- path: '/posts',
41
- operations: [
42
- {
43
- id: 'posts.list',
44
- method: 'GET',
45
- intent: 'read',
46
- parameters: [
47
- {
48
- name: 'limit',
49
- location: 'query',
50
- schema: { type: 'integer', default: 20 },
51
- },
52
- ],
53
- response: {
54
- status: 200,
55
- schema: {
56
- type: 'array',
57
- items: { ref: { id: 'post' } },
58
- },
59
- },
60
- pagination: {
61
- kind: 'limit-offset',
62
- limitParameter: 'limit',
63
- offsetParameter: 'offset',
64
- },
65
- },
66
- {
67
- id: 'posts.create',
68
- method: 'POST',
69
- intent: 'create',
70
- request: {
71
- contentType: 'application/json',
72
- schemaRef: { id: 'post' },
73
- },
74
- response: {
75
- status: 201,
76
- schemaRef: { id: 'post' },
77
- },
78
- },
79
- ],
80
- },
81
- {
82
- id: 'post',
83
19
  path: '/posts/{postId}',
84
20
  operations: [
85
21
  {
@@ -94,144 +30,57 @@ function createBlogDefinition(): ManualRestDataSourceDefinition {
94
30
  schema: { type: 'string' },
95
31
  },
96
32
  ],
97
- response: {
98
- status: 200,
99
- schemaRef: { id: 'post' },
100
- },
101
- },
102
- {
103
- id: 'posts.update',
104
- method: 'PATCH',
105
- intent: 'update',
106
- parameters: [
107
- {
108
- name: 'postId',
109
- location: 'path',
110
- required: true,
111
- schema: { type: 'string' },
112
- },
113
- ],
114
- request: {
115
- contentType: 'application/json',
116
- schemaRef: { id: 'post' },
117
- },
118
- response: {
119
- status: 200,
120
- schemaRef: { id: 'post' },
121
- },
122
- },
123
- {
124
- id: 'posts.delete',
125
- method: 'DELETE',
126
- intent: 'delete',
127
- parameters: [
128
- {
129
- name: 'postId',
130
- location: 'path',
131
- required: true,
132
- schema: { type: 'string' },
133
- },
134
- ],
135
- response: {
136
- status: 204,
137
- },
138
33
  },
34
+ { id: 'posts.create', method: 'POST', intent: 'create', path: '/posts' },
139
35
  ],
140
36
  },
141
37
  ],
142
38
  };
143
39
  }
144
40
 
145
- describe('manual REST data-source helpers', () => {
146
- it('normalizes REST methods', () => {
41
+ describe('manual REST normalization', () => {
42
+ it('normalizes methods and path parameters', () => {
147
43
  expect(normalizeManualRestMethod('get')).toBe('GET');
148
44
  expect(isManualRestMethod('GET')).toBe(true);
149
45
  expect(isManualRestMethod('TRACE')).toBe(false);
150
- });
151
-
152
- it('extracts braced and colon path parameters', () => {
153
46
  expect(extractRestPathParams('/posts/{postId}/comments/:commentId')).toEqual([
154
47
  'postId',
155
48
  'commentId',
156
49
  ]);
157
50
  });
158
51
 
159
- it('normalizes list, get, create, update, and delete operations', () => {
160
- const source = normalizeManualRestDataSource(createBlogDefinition());
161
-
162
- assertSerializable(source);
163
- expect(source.kind).toBe('rest');
164
- expect(source.endpoints.posts?.operations['posts.list']?.intent).toBe('read');
165
- expect(source.endpoints.post?.operations['posts.get']?.method).toBe('GET');
52
+ it('creates an external REST API source', () => {
53
+ const source = normalizeManualRestDataSource(definition());
54
+ expect(source).toMatchObject({ kind: 'api', origin: 'external', protocol: 'rest' });
55
+ expect(source.endpoints.posts?.operations['posts.get']?.method).toBe('GET');
166
56
  expect(source.endpoints.posts?.operations['posts.create']?.intent).toBe('create');
167
- expect(source.endpoints.post?.operations['posts.update']?.intent).toBe('update');
168
- expect(source.endpoints.post?.operations['posts.delete']?.intent).toBe('delete');
169
57
  });
170
58
 
171
- it('returns an ok diagnostic result for valid manual definitions', () => {
172
- const result = createManualRestDataSource(createBlogDefinition());
173
-
174
- expect(result.ok).toBe(true);
175
- if (result.ok) {
176
- expect(
177
- result.data.endpoints.post?.operations['posts.get']?.request?.parameters?.[0]?.name,
178
- ).toBe('postId');
179
- expect(result.diagnostics).toEqual([]);
180
- }
59
+ it('returns canonical diagnostic results', () => {
60
+ const valid = createManualRestDataSource(definition());
61
+ const invalid = createManualRestDataSource({ ...definition(), baseUrl: ' ' });
62
+ expect(valid.ok).toBe(true);
63
+ expect(invalid.ok).toBe(false);
181
64
  });
182
65
 
183
- it('reports missing base URL', () => {
66
+ it('reports unsupported methods and missing path parameters', () => {
67
+ const broken = definition();
184
68
  const result = createManualRestDataSource({
185
- ...createBlogDefinition(),
186
- baseUrl: ' ',
187
- });
188
-
189
- expect(result.ok).toBe(false);
190
- if (!result.ok) {
191
- expect(result.diagnostics.map((diagnostic) => diagnostic.path)).toContain('baseUrl');
192
- }
193
- });
194
-
195
- it('reports invalid methods and path-template mismatches', () => {
196
- const result = createManualRestDataSource({
197
- id: 'broken-api',
198
- baseUrl: 'https://api.example.com',
69
+ ...broken,
199
70
  endpoints: [
200
71
  {
201
72
  id: 'broken',
202
- path: 'broken/{id}',
203
- operations: [
204
- {
205
- id: 'broken.trace',
206
- method: 'TRACE',
207
- intent: 'read',
208
- parameters: [
209
- {
210
- name: 'unusedId',
211
- location: 'path',
212
- schema: { type: 'string' },
213
- },
214
- ],
215
- },
216
- ],
73
+ path: '/broken/{id}',
74
+ operations: [{ id: 'broken.trace', method: 'TRACE', intent: 'read' }],
217
75
  },
218
76
  ],
219
77
  });
220
-
221
78
  expect(result.ok).toBe(false);
222
79
  if (!result.ok) {
223
- const messages = result.diagnostics.map((diagnostic) => diagnostic.message);
224
-
225
- expect(messages).toContain('Manual REST endpoint paths must start with `/`.');
226
- expect(messages).toContain(
227
- 'Manual REST operation method must be one of DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT.',
228
- );
229
- expect(messages).toContain(
230
- "Path template parameter 'id' must have a matching path parameter definition.",
231
- );
232
- expect(messages).toContain(
233
- "Path parameter 'unusedId' is not referenced by the operation path template.",
234
- );
80
+ expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
81
+ 'invalid-config',
82
+ 'invalid-config',
83
+ ]);
235
84
  }
236
85
  });
237
86
  });
package/src/rest/index.ts CHANGED
@@ -13,8 +13,8 @@ import type {
13
13
  DataSourceDiagnostic,
14
14
  DataSourceDiagnosticResult,
15
15
  EndpointId,
16
+ ExternalRestApiDataSourceConfig,
16
17
  OperationId,
17
- RestDataSourceConfig,
18
18
  } from '@ankhorage/contracts/data';
19
19
 
20
20
  const REST_METHODS = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'] as const;
@@ -111,52 +111,7 @@ export function validateManualRestDataSource(
111
111
  }
112
112
 
113
113
  for (const operation of endpoint.operations) {
114
- const normalizedMethod = normalizeManualRestMethod(operation.method);
115
- const operationPath = operation.path ?? endpoint.path;
116
- const templateParams = extractRestPathParams(operationPath);
117
- const pathParameters =
118
- operation.parameters?.filter((parameter) => parameter.location === 'path') ?? [];
119
- const pathParameterNames = new Set(pathParameters.map((parameter) => parameter.name));
120
-
121
- if (!isManualRestMethod(normalizedMethod)) {
122
- diagnostics.push({
123
- code: 'invalid-config',
124
- dataSourceId: definition.id,
125
- endpointId: endpoint.id,
126
- operationId: operation.id,
127
- message: `Manual REST operation method must be one of ${REST_METHODS.join(', ')}.`,
128
- path: `endpoints.${endpoint.id}.operations.${operation.id}.method`,
129
- severity: 'error',
130
- });
131
- }
132
-
133
- for (const templateParam of templateParams) {
134
- if (!pathParameterNames.has(templateParam)) {
135
- diagnostics.push({
136
- code: 'invalid-config',
137
- dataSourceId: definition.id,
138
- endpointId: endpoint.id,
139
- operationId: operation.id,
140
- message: `Path template parameter '${templateParam}' must have a matching path parameter definition.`,
141
- path: `endpoints.${endpoint.id}.operations.${operation.id}.parameters`,
142
- severity: 'error',
143
- });
144
- }
145
- }
146
-
147
- for (const pathParameter of pathParameters) {
148
- if (!templateParams.includes(pathParameter.name)) {
149
- diagnostics.push({
150
- code: 'invalid-config',
151
- dataSourceId: definition.id,
152
- endpointId: endpoint.id,
153
- operationId: operation.id,
154
- message: `Path parameter '${pathParameter.name}' is not referenced by the operation path template.`,
155
- path: `endpoints.${endpoint.id}.operations.${operation.id}.parameters.${pathParameter.name}`,
156
- severity: 'error',
157
- });
158
- }
159
- }
114
+ validateManualRestOperation(definition, endpoint, operation, diagnostics);
160
115
  }
161
116
  }
162
117
 
@@ -165,11 +120,9 @@ export function validateManualRestDataSource(
165
120
 
166
121
  export function createManualRestDataSource(
167
122
  definition: ManualRestDataSourceDefinition,
168
- ): DataSourceDiagnosticResult<RestDataSourceConfig> {
123
+ ): DataSourceDiagnosticResult<ExternalRestApiDataSourceConfig> {
169
124
  const diagnostics = validateManualRestDataSource(definition);
170
- const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === 'error');
171
-
172
- if (hasErrors) {
125
+ if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
173
126
  return { ok: false, diagnostics };
174
127
  }
175
128
 
@@ -182,22 +135,18 @@ export function createManualRestDataSource(
182
135
 
183
136
  export function normalizeManualRestDataSource(
184
137
  definition: ManualRestDataSourceDefinition,
185
- ): RestDataSourceConfig {
138
+ ): ExternalRestApiDataSourceConfig {
186
139
  const endpoints: Record<EndpointId, DataEndpointConfig> = {};
187
140
 
188
141
  for (const endpoint of definition.endpoints) {
189
142
  const operations: Record<OperationId, DataOperationConfig> = {};
190
143
 
191
144
  for (const operation of endpoint.operations) {
192
- const operationPath = operation.path ?? endpoint.path;
193
145
  const { parameters } = operation;
194
146
  const request: DataOperationRequest | undefined =
195
147
  operation.request === undefined && parameters === undefined
196
148
  ? undefined
197
- : {
198
- ...operation.request,
199
- parameters,
200
- };
149
+ : { ...operation.request, parameters };
201
150
 
202
151
  operations[operation.id] = {
203
152
  id: operation.id,
@@ -207,7 +156,7 @@ export function normalizeManualRestDataSource(
207
156
  protocol: 'http',
208
157
  intent: operation.intent,
209
158
  method: normalizeManualRestMethod(operation.method),
210
- path: operationPath,
159
+ path: operation.path ?? endpoint.path,
211
160
  request,
212
161
  response: operation.response,
213
162
  pagination: operation.pagination,
@@ -231,7 +180,9 @@ export function normalizeManualRestDataSource(
231
180
 
232
181
  return {
233
182
  id: definition.id,
234
- kind: 'rest',
183
+ kind: 'api',
184
+ origin: 'external',
185
+ protocol: 'rest',
235
186
  name: definition.name,
236
187
  description: definition.description,
237
188
  baseUrl: definition.baseUrl,
@@ -241,3 +192,55 @@ export function normalizeManualRestDataSource(
241
192
  metadata: definition.metadata,
242
193
  };
243
194
  }
195
+
196
+ function validateManualRestOperation(
197
+ definition: ManualRestDataSourceDefinition,
198
+ endpoint: ManualRestEndpointDefinition,
199
+ operation: ManualRestOperationDefinition,
200
+ diagnostics: DataSourceDiagnostic[],
201
+ ): void {
202
+ const method = normalizeManualRestMethod(operation.method);
203
+ const path = operation.path ?? endpoint.path;
204
+ const templateParams = extractRestPathParams(path);
205
+ const pathParameters =
206
+ operation.parameters?.filter((parameter) => parameter.location === 'path') ?? [];
207
+ const pathParameterNames = new Set(pathParameters.map((parameter) => parameter.name));
208
+
209
+ if (!isManualRestMethod(method)) {
210
+ diagnostics.push({
211
+ code: 'invalid-config',
212
+ dataSourceId: definition.id,
213
+ endpointId: endpoint.id,
214
+ operationId: operation.id,
215
+ message: `Manual REST operation method must be one of ${REST_METHODS.join(', ')}.`,
216
+ path: `endpoints.${endpoint.id}.operations.${operation.id}.method`,
217
+ severity: 'error',
218
+ });
219
+ }
220
+
221
+ for (const templateParam of templateParams) {
222
+ if (pathParameterNames.has(templateParam)) continue;
223
+ diagnostics.push({
224
+ code: 'invalid-config',
225
+ dataSourceId: definition.id,
226
+ endpointId: endpoint.id,
227
+ operationId: operation.id,
228
+ message: `Path template parameter '${templateParam}' must have a matching path parameter definition.`,
229
+ path: `endpoints.${endpoint.id}.operations.${operation.id}.parameters`,
230
+ severity: 'error',
231
+ });
232
+ }
233
+
234
+ for (const pathParameter of pathParameters) {
235
+ if (templateParams.includes(pathParameter.name)) continue;
236
+ diagnostics.push({
237
+ code: 'invalid-config',
238
+ dataSourceId: definition.id,
239
+ endpointId: endpoint.id,
240
+ operationId: operation.id,
241
+ message: `Path parameter '${pathParameter.name}' is not referenced by the operation path template.`,
242
+ path: `endpoints.${endpoint.id}.operations.${operation.id}.parameters.${pathParameter.name}`,
243
+ severity: 'error',
244
+ });
245
+ }
246
+ }