@ankhorage/data-sources 1.0.0 → 2.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.
- package/CHANGELOG.md +12 -0
- package/README.md +6 -41
- package/dist/ankh.provider.js +1 -1
- package/dist/ankh.provider.js.map +1 -1
- package/dist/discovery/http.d.ts +10 -0
- package/dist/discovery/http.d.ts.map +1 -0
- package/dist/discovery/http.js +35 -0
- package/dist/discovery/http.js.map +1 -0
- package/dist/discovery/index.d.ts +9 -9
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +27 -55
- package/dist/discovery/index.js.map +1 -1
- package/dist/graphql/index.d.ts +8 -76
- package/dist/graphql/index.d.ts.map +1 -1
- package/dist/graphql/index.js +17 -197
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/introspection.d.ts +5 -0
- package/dist/graphql/introspection.d.ts.map +1 -0
- package/dist/graphql/introspection.js +136 -0
- package/dist/graphql/introspection.js.map +1 -0
- package/dist/graphql/operation.d.ts +5 -0
- package/dist/graphql/operation.d.ts.map +1 -0
- package/dist/graphql/operation.js +39 -0
- package/dist/graphql/operation.js.map +1 -0
- package/dist/graphql/types.d.ts +71 -0
- package/dist/graphql/types.d.ts.map +1 -0
- package/dist/graphql/types.js +2 -0
- package/dist/graphql/types.js.map +1 -0
- package/dist/index.d.ts +1 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -34
- package/dist/index.js.map +1 -1
- package/dist/openapi/endpoints.d.ts +5 -0
- package/dist/openapi/endpoints.d.ts.map +1 -0
- package/dist/openapi/endpoints.js +77 -0
- package/dist/openapi/endpoints.js.map +1 -0
- package/dist/openapi/index.d.ts +7 -87
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +8 -267
- package/dist/openapi/index.js.map +1 -1
- package/dist/openapi/operation.d.ts +5 -0
- package/dist/openapi/operation.d.ts.map +1 -0
- package/dist/openapi/operation.js +141 -0
- package/dist/openapi/operation.js.map +1 -0
- package/dist/openapi/schema.d.ts +5 -0
- package/dist/openapi/schema.d.ts.map +1 -0
- package/dist/openapi/schema.js +84 -0
- package/dist/openapi/schema.js.map +1 -0
- package/dist/openapi/types.d.ts +82 -0
- package/dist/openapi/types.d.ts.map +1 -0
- package/dist/openapi/types.js +2 -0
- package/dist/openapi/types.js.map +1 -0
- package/dist/packageInfo.d.ts +18 -0
- package/dist/packageInfo.d.ts.map +1 -0
- package/dist/packageInfo.js +30 -0
- package/dist/packageInfo.js.map +1 -0
- package/dist/rest/index.d.ts +5 -5
- package/dist/rest/index.d.ts.map +1 -1
- package/dist/rest/index.js +49 -48
- package/dist/rest/index.js.map +1 -1
- package/dist/test-runner/index.d.ts +2 -57
- package/dist/test-runner/index.d.ts.map +1 -1
- package/dist/test-runner/index.js +58 -160
- package/dist/test-runner/index.js.map +1 -1
- package/dist/test-runner/response.d.ts +3 -0
- package/dist/test-runner/response.d.ts.map +1 -0
- package/dist/test-runner/response.js +29 -0
- package/dist/test-runner/response.js.map +1 -0
- package/dist/test-runner/result.d.ts +6 -0
- package/dist/test-runner/result.d.ts.map +1 -0
- package/dist/test-runner/result.js +17 -0
- package/dist/test-runner/result.js.map +1 -0
- package/dist/test-runner/serialization.d.ts +12 -0
- package/dist/test-runner/serialization.d.ts.map +1 -0
- package/dist/test-runner/serialization.js +99 -0
- package/dist/test-runner/serialization.js.map +1 -0
- package/dist/test-runner/types.d.ts +58 -0
- package/dist/test-runner/types.d.ts.map +1 -0
- package/dist/test-runner/types.js +2 -0
- package/dist/test-runner/types.js.map +1 -0
- package/package.json +5 -5
- package/src/ankh.provider.ts +1 -1
- package/src/api-phase1.contract.test.ts +24 -0
- package/src/discovery/http.ts +38 -0
- package/src/discovery/index.test.ts +76 -75
- package/src/discovery/index.ts +49 -72
- package/src/graphql/index.test.ts +10 -11
- package/src/graphql/index.ts +35 -315
- package/src/graphql/introspection.ts +163 -0
- package/src/graphql/operation.ts +60 -0
- package/src/graphql/types.ts +79 -0
- package/src/index.test.ts +21 -13
- package/src/index.ts +1 -54
- package/src/openapi/endpoints.ts +124 -0
- package/src/openapi/index.test.ts +47 -47
- package/src/openapi/index.ts +12 -467
- package/src/openapi/operation.ts +231 -0
- package/src/openapi/schema.ts +127 -0
- package/src/openapi/types.ts +93 -0
- package/src/packageInfo.ts +50 -0
- package/src/rest/index.test.ts +17 -14
- package/src/rest/index.ts +67 -61
- package/src/test-runner/index.test.ts +153 -120
- package/src/test-runner/index.ts +97 -299
- package/src/test-runner/response.ts +32 -0
- package/src/test-runner/result.ts +35 -0
- package/src/test-runner/serialization.ts +151 -0
- package/src/test-runner/types.ts +81 -0
- package/dist/generated-api/index.d.ts +0 -10
- package/dist/generated-api/index.d.ts.map +0 -1
- package/dist/generated-api/index.js +0 -272
- package/dist/generated-api/index.js.map +0 -1
- package/src/generated-api/index.test.ts +0 -173
- package/src/generated-api/index.ts +0 -410
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataOperationConfig,
|
|
3
|
+
DataOperationIntent,
|
|
4
|
+
DataOperationParameter,
|
|
5
|
+
DataOperationParameterLocation,
|
|
6
|
+
DataOperationRequest,
|
|
7
|
+
DataOperationResponse,
|
|
8
|
+
DataSourceDiagnostic,
|
|
9
|
+
EndpointId,
|
|
10
|
+
OperationId,
|
|
11
|
+
} from '@ankhorage/contracts/data';
|
|
12
|
+
|
|
13
|
+
import { normalizeOpenApiSchema } from './schema';
|
|
14
|
+
import type {
|
|
15
|
+
OpenApiHttpMethod,
|
|
16
|
+
OpenApiImportInput,
|
|
17
|
+
OpenApiMediaTypeObject,
|
|
18
|
+
OpenApiOperationObject,
|
|
19
|
+
OpenApiParameterObject,
|
|
20
|
+
OpenApiPathItemObject,
|
|
21
|
+
OpenApiRequestBodyObject,
|
|
22
|
+
OpenApiResponseObject,
|
|
23
|
+
OpenApiSchemaObject,
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
export function normalizeOpenApiOperationId(
|
|
27
|
+
method: OpenApiHttpMethod,
|
|
28
|
+
path: string,
|
|
29
|
+
operationId?: string,
|
|
30
|
+
): OperationId {
|
|
31
|
+
const rawId = operationId ?? `${method}-${path}`;
|
|
32
|
+
const normalized = rawId
|
|
33
|
+
.replace(/\{([^}]+)\}/g, '$1')
|
|
34
|
+
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
35
|
+
.replace(/^-+|-+$/g, '')
|
|
36
|
+
.toLowerCase();
|
|
37
|
+
return normalized.length > 0 ? normalized : `${method}-root`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeOpenApiOperation(
|
|
41
|
+
input: OpenApiImportInput,
|
|
42
|
+
endpointId: EndpointId,
|
|
43
|
+
path: string,
|
|
44
|
+
method: OpenApiHttpMethod,
|
|
45
|
+
operation: OpenApiOperationObject,
|
|
46
|
+
pathItem: OpenApiPathItemObject,
|
|
47
|
+
operationIds: Set<OperationId>,
|
|
48
|
+
diagnostics: DataSourceDiagnostic[],
|
|
49
|
+
): DataOperationConfig {
|
|
50
|
+
const operationId = resolveUniqueOperationId(
|
|
51
|
+
input.id,
|
|
52
|
+
endpointId,
|
|
53
|
+
method,
|
|
54
|
+
path,
|
|
55
|
+
operation.operationId,
|
|
56
|
+
operationIds,
|
|
57
|
+
diagnostics,
|
|
58
|
+
);
|
|
59
|
+
const parameters = normalizeOpenApiParameters(
|
|
60
|
+
[...(pathItem.parameters ?? []), ...(operation.parameters ?? [])],
|
|
61
|
+
input.id,
|
|
62
|
+
endpointId,
|
|
63
|
+
operationId,
|
|
64
|
+
diagnostics,
|
|
65
|
+
);
|
|
66
|
+
return {
|
|
67
|
+
id: operationId,
|
|
68
|
+
endpointId,
|
|
69
|
+
name: operation.summary,
|
|
70
|
+
description: operation.description,
|
|
71
|
+
protocol: 'http',
|
|
72
|
+
intent: mapOpenApiMethodToIntent(method),
|
|
73
|
+
method: method.toUpperCase(),
|
|
74
|
+
path,
|
|
75
|
+
request: normalizeOpenApiRequest(operation.requestBody, parameters),
|
|
76
|
+
response: normalizeOpenApiResponse(operation.responses),
|
|
77
|
+
metadata: { deprecated: operation.deprecated ?? false, source: 'openapi' },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveUniqueOperationId(
|
|
82
|
+
apiId: string,
|
|
83
|
+
endpointId: EndpointId,
|
|
84
|
+
method: OpenApiHttpMethod,
|
|
85
|
+
path: string,
|
|
86
|
+
operationId: string | undefined,
|
|
87
|
+
operationIds: Set<OperationId>,
|
|
88
|
+
diagnostics: DataSourceDiagnostic[],
|
|
89
|
+
): OperationId {
|
|
90
|
+
const normalized = normalizeOpenApiOperationId(method, path, operationId);
|
|
91
|
+
if (!operationIds.has(normalized)) {
|
|
92
|
+
operationIds.add(normalized);
|
|
93
|
+
return normalized;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const uniqueId = findUniqueOperationId(normalized, operationIds);
|
|
97
|
+
operationIds.add(uniqueId);
|
|
98
|
+
diagnostics.push({
|
|
99
|
+
code: 'duplicate-operation-id',
|
|
100
|
+
apiId,
|
|
101
|
+
endpointId,
|
|
102
|
+
operationId: uniqueId,
|
|
103
|
+
message: `OpenAPI operation id '${normalized}' is duplicated. Normalized to '${uniqueId}'.`,
|
|
104
|
+
path: `paths.${path}.${method}.operationId`,
|
|
105
|
+
severity: 'warning',
|
|
106
|
+
});
|
|
107
|
+
return uniqueId;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function findUniqueOperationId(
|
|
111
|
+
normalized: OperationId,
|
|
112
|
+
operationIds: Set<OperationId>,
|
|
113
|
+
): OperationId {
|
|
114
|
+
let suffix = 2;
|
|
115
|
+
let candidate: OperationId = `${normalized}-${suffix}`;
|
|
116
|
+
while (operationIds.has(candidate)) {
|
|
117
|
+
suffix += 1;
|
|
118
|
+
candidate = `${normalized}-${suffix}`;
|
|
119
|
+
}
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeOpenApiParameters(
|
|
124
|
+
parameters: readonly OpenApiParameterObject[],
|
|
125
|
+
apiId: string,
|
|
126
|
+
endpointId: EndpointId,
|
|
127
|
+
operationId: OperationId,
|
|
128
|
+
diagnostics: DataSourceDiagnostic[],
|
|
129
|
+
): readonly DataOperationParameter[] | undefined {
|
|
130
|
+
if (parameters.length === 0) return undefined;
|
|
131
|
+
return parameters.flatMap((parameter) =>
|
|
132
|
+
normalizeOpenApiParameter(parameter, apiId, endpointId, operationId, diagnostics),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeOpenApiParameter(
|
|
137
|
+
parameter: OpenApiParameterObject,
|
|
138
|
+
apiId: string,
|
|
139
|
+
endpointId: EndpointId,
|
|
140
|
+
operationId: OperationId,
|
|
141
|
+
diagnostics: DataSourceDiagnostic[],
|
|
142
|
+
): readonly DataOperationParameter[] {
|
|
143
|
+
const location = normalizeParameterLocation(parameter.in);
|
|
144
|
+
if (location !== undefined) {
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
name: parameter.name,
|
|
148
|
+
location,
|
|
149
|
+
required: parameter.required,
|
|
150
|
+
description: parameter.description,
|
|
151
|
+
schema:
|
|
152
|
+
parameter.schema === undefined ? undefined : normalizeOpenApiSchema(parameter.schema),
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
diagnostics.push({
|
|
157
|
+
code: 'invalid-config',
|
|
158
|
+
apiId,
|
|
159
|
+
endpointId,
|
|
160
|
+
operationId,
|
|
161
|
+
message: `OpenAPI parameter '${parameter.name}' uses unsupported location '${parameter.in}'.`,
|
|
162
|
+
path: `paths.${endpointId}.parameters.${parameter.name}`,
|
|
163
|
+
severity: 'warning',
|
|
164
|
+
});
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeParameterLocation(location: string): DataOperationParameterLocation | undefined {
|
|
169
|
+
if (
|
|
170
|
+
location === 'cookie' ||
|
|
171
|
+
location === 'header' ||
|
|
172
|
+
location === 'path' ||
|
|
173
|
+
location === 'query'
|
|
174
|
+
) {
|
|
175
|
+
return location;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeOpenApiRequest(
|
|
181
|
+
requestBody: OpenApiRequestBodyObject | undefined,
|
|
182
|
+
parameters: readonly DataOperationParameter[] | undefined,
|
|
183
|
+
): DataOperationRequest | undefined {
|
|
184
|
+
const content = firstContentEntry(requestBody?.content);
|
|
185
|
+
if (requestBody === undefined && parameters === undefined) return undefined;
|
|
186
|
+
return {
|
|
187
|
+
contentType: content?.contentType,
|
|
188
|
+
parameters,
|
|
189
|
+
schema: content?.schema === undefined ? undefined : normalizeOpenApiSchema(content.schema),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeOpenApiResponse(
|
|
194
|
+
responses: Readonly<Record<string, OpenApiResponseObject>> | undefined,
|
|
195
|
+
): DataOperationResponse | undefined {
|
|
196
|
+
const selected = chooseResponseEntry(responses);
|
|
197
|
+
if (selected === undefined) return undefined;
|
|
198
|
+
const [status, response] = selected;
|
|
199
|
+
const content = firstContentEntry(response.content);
|
|
200
|
+
return {
|
|
201
|
+
status,
|
|
202
|
+
contentType: content?.contentType,
|
|
203
|
+
description: response.description,
|
|
204
|
+
schema: content?.schema === undefined ? undefined : normalizeOpenApiSchema(content.schema),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function chooseResponseEntry(
|
|
209
|
+
responses: Readonly<Record<string, OpenApiResponseObject>> | undefined,
|
|
210
|
+
): readonly [string, OpenApiResponseObject] | undefined {
|
|
211
|
+
if (responses === undefined) return undefined;
|
|
212
|
+
const entries = Object.entries(responses);
|
|
213
|
+
return entries.find(([status]) => status.startsWith('2')) ?? entries[0];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function firstContentEntry(
|
|
217
|
+
content: Readonly<Record<string, OpenApiMediaTypeObject>> | undefined,
|
|
218
|
+
): { readonly contentType: string; readonly schema?: OpenApiSchemaObject } | undefined {
|
|
219
|
+
const entry = content === undefined ? undefined : Object.entries(content)[0];
|
|
220
|
+
if (entry === undefined) return undefined;
|
|
221
|
+
const [contentType, media] = entry;
|
|
222
|
+
return { contentType, schema: media.schema };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function mapOpenApiMethodToIntent(method: OpenApiHttpMethod): DataOperationIntent {
|
|
226
|
+
if (method === 'get' || method === 'head') return 'read';
|
|
227
|
+
if (method === 'post') return 'create';
|
|
228
|
+
if (method === 'delete') return 'delete';
|
|
229
|
+
if (method === 'patch' || method === 'put') return 'update';
|
|
230
|
+
return 'action';
|
|
231
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataSchema,
|
|
3
|
+
DataSchemaPrimitiveType,
|
|
4
|
+
DataSchemaRegistry,
|
|
5
|
+
DataSourceDiagnostic,
|
|
6
|
+
} from '@ankhorage/contracts/data';
|
|
7
|
+
|
|
8
|
+
import type { OpenApiSchemaObject } from './types';
|
|
9
|
+
|
|
10
|
+
const DATA_SCHEMA_PRIMITIVES = [
|
|
11
|
+
'array',
|
|
12
|
+
'boolean',
|
|
13
|
+
'integer',
|
|
14
|
+
'null',
|
|
15
|
+
'number',
|
|
16
|
+
'object',
|
|
17
|
+
'string',
|
|
18
|
+
] as const satisfies readonly DataSchemaPrimitiveType[];
|
|
19
|
+
|
|
20
|
+
export function normalizeOpenApiSchema(schema: OpenApiSchemaObject): DataSchema {
|
|
21
|
+
return {
|
|
22
|
+
ref: normalizeOpenApiSchemaRef(schema.$ref),
|
|
23
|
+
type: normalizeSchemaType(schema.type),
|
|
24
|
+
title: schema.title,
|
|
25
|
+
description: schema.description,
|
|
26
|
+
enum: schema.enum,
|
|
27
|
+
const: schema.const,
|
|
28
|
+
default: schema.default,
|
|
29
|
+
format: schema.format,
|
|
30
|
+
nullable: schema.nullable,
|
|
31
|
+
required: schema.required,
|
|
32
|
+
properties: normalizeSchemaRecord(schema.properties),
|
|
33
|
+
additionalProperties: normalizeAdditionalProperties(schema.additionalProperties),
|
|
34
|
+
items: schema.items === undefined ? undefined : normalizeOpenApiSchema(schema.items),
|
|
35
|
+
allOf: normalizeSchemaList(schema.allOf),
|
|
36
|
+
anyOf: normalizeSchemaList(schema.anyOf),
|
|
37
|
+
oneOf: normalizeSchemaList(schema.oneOf),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeOpenApiSchemas(
|
|
42
|
+
schemas: Readonly<Record<string, OpenApiSchemaObject>> | undefined,
|
|
43
|
+
apiId: string,
|
|
44
|
+
diagnostics: DataSourceDiagnostic[],
|
|
45
|
+
): DataSchemaRegistry | undefined {
|
|
46
|
+
if (schemas === undefined) return undefined;
|
|
47
|
+
return Object.fromEntries(
|
|
48
|
+
Object.entries(schemas).map(([schemaId, schema]) => {
|
|
49
|
+
collectUnsupportedSchemaDiagnostics(
|
|
50
|
+
schema,
|
|
51
|
+
apiId,
|
|
52
|
+
`components.schemas.${schemaId}`,
|
|
53
|
+
diagnostics,
|
|
54
|
+
);
|
|
55
|
+
return [schemaId, normalizeOpenApiSchema(schema)];
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeOpenApiSchemaRef(ref: string | undefined): { readonly id: string } | undefined {
|
|
61
|
+
if (ref === undefined) return undefined;
|
|
62
|
+
const prefix = '#/components/schemas/';
|
|
63
|
+
return { id: ref.startsWith(prefix) ? ref.slice(prefix.length) : ref };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeSchemaRecord(
|
|
67
|
+
record: Readonly<Record<string, OpenApiSchemaObject>> | undefined,
|
|
68
|
+
): Readonly<Record<string, DataSchema>> | undefined {
|
|
69
|
+
if (record === undefined) return undefined;
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
Object.entries(record).map(([key, schema]) => [key, normalizeOpenApiSchema(schema)]),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeSchemaList(
|
|
76
|
+
list: readonly OpenApiSchemaObject[] | undefined,
|
|
77
|
+
): readonly DataSchema[] | undefined {
|
|
78
|
+
return list?.map(normalizeOpenApiSchema);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeAdditionalProperties(
|
|
82
|
+
value: boolean | OpenApiSchemaObject | undefined,
|
|
83
|
+
): boolean | DataSchema | undefined {
|
|
84
|
+
if (value === undefined || typeof value === 'boolean') return value;
|
|
85
|
+
return normalizeOpenApiSchema(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeSchemaType(
|
|
89
|
+
type: string | readonly string[] | undefined,
|
|
90
|
+
): DataSchemaPrimitiveType | readonly DataSchemaPrimitiveType[] | undefined {
|
|
91
|
+
if (type === undefined) return undefined;
|
|
92
|
+
if (typeof type === 'string') return isDataSchemaPrimitive(type) ? type : undefined;
|
|
93
|
+
return type.filter(isDataSchemaPrimitive);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isDataSchemaPrimitive(type: string): type is DataSchemaPrimitiveType {
|
|
97
|
+
return DATA_SCHEMA_PRIMITIVES.some((primitive) => primitive === type);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function collectUnsupportedSchemaDiagnostics(
|
|
101
|
+
schema: OpenApiSchemaObject,
|
|
102
|
+
apiId: string,
|
|
103
|
+
path: string,
|
|
104
|
+
diagnostics: DataSourceDiagnostic[],
|
|
105
|
+
): void {
|
|
106
|
+
if (typeof schema.type === 'string' && !isDataSchemaPrimitive(schema.type)) {
|
|
107
|
+
diagnostics.push({
|
|
108
|
+
code: 'unsupported-schema',
|
|
109
|
+
apiId,
|
|
110
|
+
message: `OpenAPI schema type '${schema.type}' is not supported by the normalized schema model.`,
|
|
111
|
+
path: `${path}.type`,
|
|
112
|
+
severity: 'warning',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const [propertyName, propertySchema] of Object.entries(schema.properties ?? {})) {
|
|
117
|
+
collectUnsupportedSchemaDiagnostics(
|
|
118
|
+
propertySchema,
|
|
119
|
+
apiId,
|
|
120
|
+
`${path}.properties.${propertyName}`,
|
|
121
|
+
diagnostics,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (schema.items !== undefined) {
|
|
125
|
+
collectUnsupportedSchemaDiagnostics(schema.items, apiId, `${path}.items`, diagnostics);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { CredentialRef, DataContractValue } from '@ankhorage/contracts/data';
|
|
2
|
+
|
|
3
|
+
export type OpenApiHttpMethod = 'delete' | 'get' | 'head' | 'options' | 'patch' | 'post' | 'put';
|
|
4
|
+
|
|
5
|
+
export interface OpenApiServerObject {
|
|
6
|
+
readonly url: string;
|
|
7
|
+
readonly description?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OpenApiSchemaObject {
|
|
11
|
+
readonly $ref?: string;
|
|
12
|
+
readonly type?: string | readonly string[];
|
|
13
|
+
readonly title?: string;
|
|
14
|
+
readonly description?: string;
|
|
15
|
+
readonly enum?: readonly DataContractValue[];
|
|
16
|
+
readonly const?: DataContractValue;
|
|
17
|
+
readonly default?: DataContractValue;
|
|
18
|
+
readonly format?: string;
|
|
19
|
+
readonly nullable?: boolean;
|
|
20
|
+
readonly required?: readonly string[];
|
|
21
|
+
readonly properties?: Readonly<Record<string, OpenApiSchemaObject>>;
|
|
22
|
+
readonly additionalProperties?: boolean | OpenApiSchemaObject;
|
|
23
|
+
readonly items?: OpenApiSchemaObject;
|
|
24
|
+
readonly allOf?: readonly OpenApiSchemaObject[];
|
|
25
|
+
readonly anyOf?: readonly OpenApiSchemaObject[];
|
|
26
|
+
readonly oneOf?: readonly OpenApiSchemaObject[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OpenApiMediaTypeObject {
|
|
30
|
+
readonly schema?: OpenApiSchemaObject;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface OpenApiRequestBodyObject {
|
|
34
|
+
readonly description?: string;
|
|
35
|
+
readonly required?: boolean;
|
|
36
|
+
readonly content?: Readonly<Record<string, OpenApiMediaTypeObject>>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OpenApiResponseObject {
|
|
40
|
+
readonly description?: string;
|
|
41
|
+
readonly content?: Readonly<Record<string, OpenApiMediaTypeObject>>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface OpenApiParameterObject {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly in: string;
|
|
47
|
+
readonly required?: boolean;
|
|
48
|
+
readonly description?: string;
|
|
49
|
+
readonly schema?: OpenApiSchemaObject;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface OpenApiOperationObject {
|
|
53
|
+
readonly operationId?: string;
|
|
54
|
+
readonly summary?: string;
|
|
55
|
+
readonly description?: string;
|
|
56
|
+
readonly deprecated?: boolean;
|
|
57
|
+
readonly parameters?: readonly OpenApiParameterObject[];
|
|
58
|
+
readonly requestBody?: OpenApiRequestBodyObject;
|
|
59
|
+
readonly responses?: Readonly<Record<string, OpenApiResponseObject>>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type OpenApiPathItemObject = Partial<Record<OpenApiHttpMethod, OpenApiOperationObject>> & {
|
|
63
|
+
readonly parameters?: readonly OpenApiParameterObject[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface OpenApiComponentsObject {
|
|
67
|
+
readonly schemas?: Readonly<Record<string, OpenApiSchemaObject>>;
|
|
68
|
+
readonly securitySchemes?: Readonly<Record<string, DataContractValue>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface OpenApiDocumentObject {
|
|
72
|
+
readonly openapi?: string;
|
|
73
|
+
readonly info?: {
|
|
74
|
+
readonly title?: string;
|
|
75
|
+
readonly version?: string;
|
|
76
|
+
readonly description?: string;
|
|
77
|
+
};
|
|
78
|
+
readonly servers?: readonly OpenApiServerObject[];
|
|
79
|
+
readonly paths?: Readonly<Record<string, OpenApiPathItemObject>>;
|
|
80
|
+
readonly components?: OpenApiComponentsObject;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface OpenApiImportInput {
|
|
84
|
+
readonly id: string;
|
|
85
|
+
readonly document: OpenApiDocumentObject;
|
|
86
|
+
readonly baseUrl?: string;
|
|
87
|
+
readonly credential?: CredentialRef;
|
|
88
|
+
readonly documentId?: string;
|
|
89
|
+
readonly documentUrl?: string;
|
|
90
|
+
readonly name?: string;
|
|
91
|
+
readonly description?: string;
|
|
92
|
+
readonly metadata?: DataContractValue;
|
|
93
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiOrigin,
|
|
3
|
+
ApiProtocol,
|
|
4
|
+
DataSourceConfig,
|
|
5
|
+
DataSourceKind,
|
|
6
|
+
} from '@ankhorage/contracts/data';
|
|
7
|
+
|
|
8
|
+
export const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;
|
|
9
|
+
export const SUPPORTED_DATA_SOURCE_KINDS = [
|
|
10
|
+
'database',
|
|
11
|
+
] as const satisfies readonly DataSourceKind[];
|
|
12
|
+
export const SUPPORTED_API_ORIGINS = ['external'] as const satisfies readonly ApiOrigin[];
|
|
13
|
+
export const SUPPORTED_API_PROTOCOLS = [
|
|
14
|
+
'graphql',
|
|
15
|
+
'rest',
|
|
16
|
+
] as const satisfies readonly ApiProtocol[];
|
|
17
|
+
|
|
18
|
+
export type SupportedApiOrigin = (typeof SUPPORTED_API_ORIGINS)[number];
|
|
19
|
+
|
|
20
|
+
export interface DataSourcesPackageInfo {
|
|
21
|
+
readonly packageName: typeof DATA_SOURCES_PACKAGE_NAME;
|
|
22
|
+
readonly supportedKinds: typeof SUPPORTED_DATA_SOURCE_KINDS;
|
|
23
|
+
readonly supportedApiOrigins: typeof SUPPORTED_API_ORIGINS;
|
|
24
|
+
readonly supportedApiProtocols: typeof SUPPORTED_API_PROTOCOLS;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDataSourcesPackageInfo(): DataSourcesPackageInfo {
|
|
28
|
+
return {
|
|
29
|
+
packageName: DATA_SOURCES_PACKAGE_NAME,
|
|
30
|
+
supportedKinds: SUPPORTED_DATA_SOURCE_KINDS,
|
|
31
|
+
supportedApiOrigins: SUPPORTED_API_ORIGINS,
|
|
32
|
+
supportedApiProtocols: SUPPORTED_API_PROTOCOLS,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isSupportedDataSourceKind(kind: string): kind is DataSourceKind {
|
|
37
|
+
return SUPPORTED_DATA_SOURCE_KINDS.some((supported) => supported === kind);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isSupportedApiOrigin(origin: string): origin is SupportedApiOrigin {
|
|
41
|
+
return SUPPORTED_API_ORIGINS.some((supported) => supported === origin);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isSupportedApiProtocol(protocol: string): protocol is ApiProtocol {
|
|
45
|
+
return SUPPORTED_API_PROTOCOLS.some((supported) => supported === protocol);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getDataSourceKind(source: DataSourceConfig): DataSourceKind {
|
|
49
|
+
return source.kind;
|
|
50
|
+
}
|
package/src/rest/index.test.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
createManualRestApi,
|
|
5
5
|
extractRestPathParams,
|
|
6
6
|
isManualRestMethod,
|
|
7
|
-
type
|
|
8
|
-
|
|
7
|
+
type ManualRestApiDefinition,
|
|
8
|
+
normalizeManualRestApi,
|
|
9
9
|
normalizeManualRestMethod,
|
|
10
10
|
} from './index';
|
|
11
11
|
|
|
12
|
-
function definition():
|
|
12
|
+
function definition(): ManualRestApiDefinition {
|
|
13
13
|
return {
|
|
14
14
|
id: 'blog-api',
|
|
15
15
|
baseUrl: 'https://api.example.com',
|
|
@@ -38,7 +38,7 @@ function definition(): ManualRestDataSourceDefinition {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
describe('manual REST normalization', () => {
|
|
41
|
+
describe('manual REST API normalization', () => {
|
|
42
42
|
it('normalizes methods and path parameters', () => {
|
|
43
43
|
expect(normalizeManualRestMethod('get')).toBe('GET');
|
|
44
44
|
expect(isManualRestMethod('GET')).toBe(true);
|
|
@@ -49,23 +49,26 @@ describe('manual REST normalization', () => {
|
|
|
49
49
|
]);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
it('creates
|
|
53
|
-
const
|
|
54
|
-
expect(
|
|
55
|
-
expect(
|
|
56
|
-
expect(
|
|
52
|
+
it('creates a canonical external REST API', () => {
|
|
53
|
+
const api = normalizeManualRestApi(definition());
|
|
54
|
+
expect(api).toMatchObject({ origin: 'external', protocol: 'rest' });
|
|
55
|
+
expect('kind' in api).toBe(false);
|
|
56
|
+
expect(api.endpoints.posts?.baseUrl).toBeUndefined();
|
|
57
|
+
expect(api.endpoints.posts?.operations['posts.get']?.method).toBe('GET');
|
|
58
|
+
expect(api.endpoints.posts?.operations['posts.create']?.intent).toBe('create');
|
|
57
59
|
});
|
|
58
60
|
|
|
59
|
-
it('returns canonical
|
|
60
|
-
const valid =
|
|
61
|
-
const invalid =
|
|
61
|
+
it('returns canonical diagnostics with API identity', () => {
|
|
62
|
+
const valid = createManualRestApi(definition());
|
|
63
|
+
const invalid = createManualRestApi({ ...definition(), baseUrl: ' ' });
|
|
62
64
|
expect(valid.ok).toBe(true);
|
|
63
65
|
expect(invalid.ok).toBe(false);
|
|
66
|
+
if (!invalid.ok) expect(invalid.diagnostics[0]?.apiId).toBe('blog-api');
|
|
64
67
|
});
|
|
65
68
|
|
|
66
69
|
it('reports unsupported methods and missing path parameters', () => {
|
|
67
70
|
const broken = definition();
|
|
68
|
-
const result =
|
|
71
|
+
const result = createManualRestApi({
|
|
69
72
|
...broken,
|
|
70
73
|
endpoints: [
|
|
71
74
|
{
|