@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.
- package/CHANGELOG.md +12 -0
- package/README.md +5 -5
- package/dist/discovery/index.d.ts +3 -3
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +19 -28
- package/dist/discovery/index.js.map +1 -1
- 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 +274 -0
- package/dist/generated-api/index.js.map +1 -0
- package/dist/generated-api/validateGeneratedApiCollection.d.ts +3 -0
- package/dist/generated-api/validateGeneratedApiCollection.d.ts.map +1 -0
- package/dist/generated-api/validateGeneratedApiCollection.js +35 -0
- package/dist/generated-api/validateGeneratedApiCollection.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 +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -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 +25 -101
- package/src/discovery/index.ts +22 -35
- package/src/{managed-api → generated-api}/index.test.ts +59 -67
- package/src/generated-api/index.ts +414 -0
- package/src/generated-api/validateGeneratedApiCollection.test.ts +91 -0
- package/src/generated-api/validateGeneratedApiCollection.ts +76 -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 +28 -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
package/src/rest/index.test.ts
CHANGED
|
@@ -9,77 +9,13 @@ import {
|
|
|
9
9
|
normalizeManualRestMethod,
|
|
10
10
|
} from './index';
|
|
11
11
|
|
|
12
|
-
function
|
|
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
|
|
146
|
-
it('normalizes
|
|
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('
|
|
160
|
-
const source = normalizeManualRestDataSource(
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
expect(
|
|
175
|
-
|
|
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
|
|
66
|
+
it('reports unsupported methods and missing path parameters', () => {
|
|
67
|
+
const broken = definition();
|
|
184
68
|
const result = createManualRestDataSource({
|
|
185
|
-
...
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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<
|
|
123
|
+
): DataSourceDiagnosticResult<ExternalRestApiDataSourceConfig> {
|
|
169
124
|
const diagnostics = validateManualRestDataSource(definition);
|
|
170
|
-
|
|
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
|
-
):
|
|
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:
|
|
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: '
|
|
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
|
+
}
|