@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
package/src/graphql/index.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
CredentialRef,
|
|
3
|
-
DataContractValue,
|
|
4
2
|
DataEndpointConfig,
|
|
5
|
-
DataOperationConfig,
|
|
6
|
-
DataOperationIntent,
|
|
7
|
-
DataOperationResponse,
|
|
8
|
-
DataSchema,
|
|
9
|
-
DataSchemaRegistry,
|
|
10
3
|
DataSourceDiagnostic,
|
|
11
4
|
DataSourceDiagnosticResult,
|
|
12
|
-
|
|
13
|
-
OperationId,
|
|
5
|
+
ExternalGraphQlApiDefinition,
|
|
14
6
|
} from '@ankhorage/contracts/data';
|
|
15
7
|
|
|
8
|
+
import {
|
|
9
|
+
normalizeGraphQlIntrospectionOperations,
|
|
10
|
+
normalizeGraphQlIntrospectionSchemas,
|
|
11
|
+
} from './introspection';
|
|
12
|
+
import { normalizeGraphQlOperation } from './operation';
|
|
13
|
+
import type { GraphQlApiDefinition } from './types';
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
normalizeGraphQlIntrospectionOperations,
|
|
17
|
+
normalizeGraphQlIntrospectionSchemas,
|
|
18
|
+
} from './introspection';
|
|
19
|
+
export { normalizeGraphQlOperationId } from './operation';
|
|
20
|
+
export type * from './types';
|
|
21
|
+
|
|
16
22
|
export const GRAPHQL_INTROSPECTION_QUERY = `query AnkhorageGraphQlIntrospection {
|
|
17
23
|
__schema {
|
|
18
24
|
queryType { name }
|
|
@@ -45,80 +51,6 @@ export const GRAPHQL_INTROSPECTION_QUERY = `query AnkhorageGraphQlIntrospection
|
|
|
45
51
|
}
|
|
46
52
|
}`;
|
|
47
53
|
|
|
48
|
-
export type GraphQlOperationKind = 'mutation' | 'query' | 'subscription';
|
|
49
|
-
type DataContractRecord = Record<string, DataContractValue>;
|
|
50
|
-
|
|
51
|
-
export interface GraphQlIntrospectionTypeRef {
|
|
52
|
-
readonly kind: string;
|
|
53
|
-
readonly name?: string | null;
|
|
54
|
-
readonly ofType?: GraphQlIntrospectionTypeRef | null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface GraphQlIntrospectionInputValue {
|
|
58
|
-
readonly name: string;
|
|
59
|
-
readonly description?: string | null;
|
|
60
|
-
readonly type: GraphQlIntrospectionTypeRef;
|
|
61
|
-
readonly defaultValue?: string | null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface GraphQlIntrospectionField {
|
|
65
|
-
readonly name: string;
|
|
66
|
-
readonly description?: string | null;
|
|
67
|
-
readonly args?: readonly GraphQlIntrospectionInputValue[] | null;
|
|
68
|
-
readonly type: GraphQlIntrospectionTypeRef;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface GraphQlIntrospectionEnumValue {
|
|
72
|
-
readonly name: string;
|
|
73
|
-
readonly description?: string | null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface GraphQlIntrospectionType {
|
|
77
|
-
readonly kind: string;
|
|
78
|
-
readonly name?: string | null;
|
|
79
|
-
readonly description?: string | null;
|
|
80
|
-
readonly fields?: readonly GraphQlIntrospectionField[] | null;
|
|
81
|
-
readonly inputFields?: readonly GraphQlIntrospectionInputValue[] | null;
|
|
82
|
-
readonly enumValues?: readonly GraphQlIntrospectionEnumValue[] | null;
|
|
83
|
-
readonly possibleTypes?: readonly GraphQlIntrospectionTypeRef[] | null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface GraphQlIntrospectionSchema {
|
|
87
|
-
readonly queryType?: { readonly name?: string | null } | null;
|
|
88
|
-
readonly mutationType?: { readonly name?: string | null } | null;
|
|
89
|
-
readonly subscriptionType?: { readonly name?: string | null } | null;
|
|
90
|
-
readonly types?: readonly GraphQlIntrospectionType[] | null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface GraphQlIntrospectionResult {
|
|
94
|
-
readonly __schema?: GraphQlIntrospectionSchema;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface GraphQlOperationDefinition {
|
|
98
|
-
readonly id: OperationId;
|
|
99
|
-
readonly kind: GraphQlOperationKind;
|
|
100
|
-
readonly name?: string;
|
|
101
|
-
readonly description?: string;
|
|
102
|
-
readonly document?: string;
|
|
103
|
-
readonly variables?: DataSchema;
|
|
104
|
-
readonly response?: DataSchema;
|
|
105
|
-
readonly selectionPath?: string;
|
|
106
|
-
readonly metadata?: DataContractValue;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export interface GraphQlDataSourceDefinition {
|
|
110
|
-
readonly id: string;
|
|
111
|
-
readonly endpointUrl: string;
|
|
112
|
-
readonly name?: string;
|
|
113
|
-
readonly description?: string;
|
|
114
|
-
readonly credential?: CredentialRef;
|
|
115
|
-
readonly introspection?: GraphQlIntrospectionResult;
|
|
116
|
-
readonly introspectionEnabled?: boolean;
|
|
117
|
-
readonly schemaVersion?: string;
|
|
118
|
-
readonly operations?: readonly GraphQlOperationDefinition[];
|
|
119
|
-
readonly metadata?: DataContractValue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
54
|
export interface GraphQlIntrospectionRequest {
|
|
123
55
|
readonly query: typeof GRAPHQL_INTROSPECTION_QUERY;
|
|
124
56
|
readonly operationName: 'AnkhorageGraphQlIntrospection';
|
|
@@ -131,91 +63,72 @@ export function createGraphQlIntrospectionRequest(): GraphQlIntrospectionRequest
|
|
|
131
63
|
};
|
|
132
64
|
}
|
|
133
65
|
|
|
134
|
-
export function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.toLowerCase();
|
|
139
|
-
return normalizedName.length > 0 ? `${kind}.${normalizedName}` : `${kind}.operation`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function createGraphQlDataSource(
|
|
143
|
-
definition: GraphQlDataSourceDefinition,
|
|
144
|
-
): DataSourceDiagnosticResult<ExternalGraphQlApiDataSourceConfig> {
|
|
145
|
-
const diagnostics = validateGraphQlDataSource(definition);
|
|
66
|
+
export function createGraphQlApi(
|
|
67
|
+
definition: GraphQlApiDefinition,
|
|
68
|
+
): DataSourceDiagnosticResult<ExternalGraphQlApiDefinition> {
|
|
69
|
+
const diagnostics = validateGraphQlApi(definition);
|
|
146
70
|
if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
|
147
71
|
return { ok: false, diagnostics };
|
|
148
72
|
}
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
ok: true,
|
|
152
|
-
data: normalizeGraphQlDataSource(definition),
|
|
153
|
-
diagnostics,
|
|
154
|
-
};
|
|
73
|
+
return { ok: true, data: normalizeGraphQlApi(definition), diagnostics };
|
|
155
74
|
}
|
|
156
75
|
|
|
157
|
-
export function
|
|
158
|
-
definition:
|
|
76
|
+
export function validateGraphQlApi(
|
|
77
|
+
definition: GraphQlApiDefinition,
|
|
159
78
|
): readonly DataSourceDiagnostic[] {
|
|
160
79
|
const diagnostics: DataSourceDiagnostic[] = [];
|
|
161
80
|
|
|
162
81
|
if (definition.endpointUrl.trim().length === 0) {
|
|
163
82
|
diagnostics.push({
|
|
164
83
|
code: 'invalid-config',
|
|
165
|
-
|
|
166
|
-
message: 'GraphQL
|
|
84
|
+
apiId: definition.id,
|
|
85
|
+
message: 'GraphQL API requires a non-empty endpointUrl.',
|
|
167
86
|
path: 'endpointUrl',
|
|
168
87
|
severity: 'error',
|
|
169
88
|
});
|
|
170
89
|
}
|
|
171
|
-
|
|
172
90
|
if (definition.introspectionEnabled === false && definition.introspection !== undefined) {
|
|
173
91
|
diagnostics.push({
|
|
174
92
|
code: 'invalid-config',
|
|
175
|
-
|
|
93
|
+
apiId: definition.id,
|
|
176
94
|
message: 'GraphQL introspection data was provided while introspection is disabled.',
|
|
177
95
|
path: 'introspection',
|
|
178
96
|
severity: 'warning',
|
|
179
97
|
});
|
|
180
98
|
}
|
|
181
|
-
|
|
182
99
|
if (definition.introspectionEnabled !== false && definition.introspection === undefined) {
|
|
183
100
|
diagnostics.push({
|
|
184
101
|
code: 'missing-schema',
|
|
185
|
-
|
|
102
|
+
apiId: definition.id,
|
|
186
103
|
message:
|
|
187
104
|
'GraphQL introspection result was not provided. Manual operations can still be used.',
|
|
188
105
|
path: 'introspection',
|
|
189
106
|
severity: 'info',
|
|
190
107
|
});
|
|
191
108
|
}
|
|
192
|
-
|
|
193
109
|
return diagnostics;
|
|
194
110
|
}
|
|
195
111
|
|
|
196
|
-
export function
|
|
197
|
-
definition:
|
|
198
|
-
):
|
|
112
|
+
export function normalizeGraphQlApi(
|
|
113
|
+
definition: GraphQlApiDefinition,
|
|
114
|
+
): ExternalGraphQlApiDefinition {
|
|
199
115
|
const schemas = normalizeGraphQlIntrospectionSchemas(definition.introspection);
|
|
200
116
|
const discovered = normalizeGraphQlIntrospectionOperations(definition.introspection);
|
|
201
|
-
const operations
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
117
|
+
const operations = Object.fromEntries(
|
|
118
|
+
[...discovered, ...(definition.operations ?? [])].map((operation) => [
|
|
119
|
+
operation.id,
|
|
120
|
+
normalizeGraphQlOperation(operation),
|
|
121
|
+
]),
|
|
122
|
+
);
|
|
207
123
|
const endpoint: DataEndpointConfig = {
|
|
208
124
|
id: 'graphql',
|
|
209
125
|
kind: 'graphql',
|
|
210
|
-
baseUrl: definition.endpointUrl,
|
|
211
126
|
credential: definition.credential,
|
|
212
127
|
operations,
|
|
213
128
|
metadata: { source: 'graphql' },
|
|
214
129
|
};
|
|
215
|
-
|
|
216
130
|
return {
|
|
217
131
|
id: definition.id,
|
|
218
|
-
kind: 'api',
|
|
219
132
|
origin: 'external',
|
|
220
133
|
protocol: 'graphql',
|
|
221
134
|
name: definition.name,
|
|
@@ -231,196 +144,3 @@ export function normalizeGraphQlDataSource(
|
|
|
231
144
|
metadata: definition.metadata,
|
|
232
145
|
};
|
|
233
146
|
}
|
|
234
|
-
|
|
235
|
-
export function normalizeGraphQlIntrospectionSchemas(
|
|
236
|
-
introspection: GraphQlIntrospectionResult | undefined,
|
|
237
|
-
): DataSchemaRegistry | undefined {
|
|
238
|
-
const types = introspection?.__schema?.types?.filter(isNamedGraphQlType) ?? [];
|
|
239
|
-
if (types.length === 0) return undefined;
|
|
240
|
-
|
|
241
|
-
const schemas: Record<string, DataSchema> = {};
|
|
242
|
-
for (const type of types) {
|
|
243
|
-
if (type.name === undefined || type.name === null || type.name.startsWith('__')) continue;
|
|
244
|
-
schemas[type.name] = normalizeGraphQlType(type);
|
|
245
|
-
}
|
|
246
|
-
return schemas;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function normalizeGraphQlIntrospectionOperations(
|
|
250
|
-
introspection: GraphQlIntrospectionResult | undefined,
|
|
251
|
-
): readonly GraphQlOperationDefinition[] {
|
|
252
|
-
const schema = introspection?.__schema;
|
|
253
|
-
if (schema === undefined) return [];
|
|
254
|
-
|
|
255
|
-
const types = schema.types?.filter(isNamedGraphQlType) ?? [];
|
|
256
|
-
const operations: GraphQlOperationDefinition[] = [];
|
|
257
|
-
appendRootOperations(operations, 'query', schema.queryType?.name, types);
|
|
258
|
-
appendRootOperations(operations, 'mutation', schema.mutationType?.name, types);
|
|
259
|
-
appendRootOperations(operations, 'subscription', schema.subscriptionType?.name, types);
|
|
260
|
-
return operations;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function appendRootOperations(
|
|
264
|
-
operations: GraphQlOperationDefinition[],
|
|
265
|
-
kind: GraphQlOperationKind,
|
|
266
|
-
rootTypeName: string | null | undefined,
|
|
267
|
-
types: readonly GraphQlIntrospectionType[],
|
|
268
|
-
): void {
|
|
269
|
-
if (rootTypeName === undefined || rootTypeName === null) return;
|
|
270
|
-
const rootType = types.find((type) => type.name === rootTypeName);
|
|
271
|
-
|
|
272
|
-
for (const field of rootType?.fields ?? []) {
|
|
273
|
-
operations.push({
|
|
274
|
-
id: normalizeGraphQlOperationId(kind, field.name),
|
|
275
|
-
kind,
|
|
276
|
-
name: field.name,
|
|
277
|
-
description: field.description ?? undefined,
|
|
278
|
-
variables: normalizeGraphQlVariablesSchema(field.args ?? []),
|
|
279
|
-
response: normalizeGraphQlTypeRef(field.type),
|
|
280
|
-
selectionPath: `$.data.${field.name}`,
|
|
281
|
-
metadata: { rootType: rootTypeName, source: 'introspection' },
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function normalizeGraphQlOperation(operation: GraphQlOperationDefinition): DataOperationConfig {
|
|
287
|
-
const response: DataOperationResponse | undefined =
|
|
288
|
-
operation.response === undefined ? undefined : { schema: operation.response };
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
id: operation.id,
|
|
292
|
-
endpointId: 'graphql',
|
|
293
|
-
name: operation.name,
|
|
294
|
-
description: operation.description,
|
|
295
|
-
protocol: 'graphql',
|
|
296
|
-
intent: mapGraphQlOperationKindToIntent(operation.kind),
|
|
297
|
-
request: { schema: operation.variables },
|
|
298
|
-
response,
|
|
299
|
-
metadata: createGraphQlOperationMetadata(operation),
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function mapGraphQlOperationKindToIntent(kind: GraphQlOperationKind): DataOperationIntent {
|
|
304
|
-
return kind === 'query' || kind === 'subscription' ? 'read' : 'action';
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function normalizeGraphQlVariablesSchema(
|
|
308
|
-
args: readonly GraphQlIntrospectionInputValue[],
|
|
309
|
-
): DataSchema {
|
|
310
|
-
const properties: Record<string, DataSchema> = {};
|
|
311
|
-
const required: string[] = [];
|
|
312
|
-
|
|
313
|
-
for (const arg of args) {
|
|
314
|
-
properties[arg.name] = {
|
|
315
|
-
...normalizeGraphQlTypeRef(arg.type),
|
|
316
|
-
description: arg.description ?? undefined,
|
|
317
|
-
default: arg.defaultValue ?? undefined,
|
|
318
|
-
};
|
|
319
|
-
if (isGraphQlNonNull(arg.type)) required.push(arg.name);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
type: 'object',
|
|
324
|
-
required: required.length > 0 ? required : undefined,
|
|
325
|
-
properties,
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function normalizeGraphQlType(type: GraphQlIntrospectionType): DataSchema {
|
|
330
|
-
if (type.kind === 'OBJECT' || type.kind === 'INTERFACE' || type.kind === 'INPUT_OBJECT') {
|
|
331
|
-
return normalizeGraphQlObjectType(type);
|
|
332
|
-
}
|
|
333
|
-
if (type.kind === 'ENUM') {
|
|
334
|
-
return {
|
|
335
|
-
type: 'string',
|
|
336
|
-
title: type.name ?? undefined,
|
|
337
|
-
description: type.description ?? undefined,
|
|
338
|
-
enum: type.enumValues?.map((value) => value.name),
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
if (type.kind === 'SCALAR') {
|
|
342
|
-
return {
|
|
343
|
-
...normalizeGraphQlNamedScalar(type.name),
|
|
344
|
-
title: type.name ?? undefined,
|
|
345
|
-
description: type.description ?? undefined,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
if (type.kind === 'UNION') {
|
|
349
|
-
return {
|
|
350
|
-
title: type.name ?? undefined,
|
|
351
|
-
description: type.description ?? undefined,
|
|
352
|
-
anyOf: type.possibleTypes?.map(normalizeGraphQlTypeRef),
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
return { title: type.name ?? undefined, description: type.description ?? undefined };
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function normalizeGraphQlObjectType(type: GraphQlIntrospectionType): DataSchema {
|
|
359
|
-
const fields = type.kind === 'INPUT_OBJECT' ? type.inputFields : type.fields;
|
|
360
|
-
const properties: Record<string, DataSchema> = {};
|
|
361
|
-
const required: string[] = [];
|
|
362
|
-
|
|
363
|
-
for (const field of fields ?? []) {
|
|
364
|
-
const fieldType = field.type;
|
|
365
|
-
properties[field.name] = {
|
|
366
|
-
...normalizeGraphQlTypeRef(fieldType),
|
|
367
|
-
description: field.description ?? undefined,
|
|
368
|
-
};
|
|
369
|
-
if (isGraphQlNonNull(fieldType)) required.push(field.name);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return {
|
|
373
|
-
type: 'object',
|
|
374
|
-
title: type.name ?? undefined,
|
|
375
|
-
description: type.description ?? undefined,
|
|
376
|
-
required: required.length > 0 ? required : undefined,
|
|
377
|
-
properties,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function normalizeGraphQlTypeRef(type: GraphQlIntrospectionTypeRef): DataSchema {
|
|
382
|
-
if (type.kind === 'NON_NULL' && type.ofType !== undefined && type.ofType !== null) {
|
|
383
|
-
return { ...normalizeGraphQlTypeRef(type.ofType), nullable: false };
|
|
384
|
-
}
|
|
385
|
-
if (type.kind === 'LIST' && type.ofType !== undefined && type.ofType !== null) {
|
|
386
|
-
return { type: 'array', items: normalizeGraphQlTypeRef(type.ofType) };
|
|
387
|
-
}
|
|
388
|
-
if (type.kind === 'SCALAR') return normalizeGraphQlNamedScalar(type.name);
|
|
389
|
-
if (type.name !== undefined && type.name !== null) return { ref: { id: type.name } };
|
|
390
|
-
return {};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function normalizeGraphQlNamedScalar(name: string | null | undefined): DataSchema {
|
|
394
|
-
if (name === 'Boolean') return { type: 'boolean' };
|
|
395
|
-
if (name === 'Float') return { type: 'number' };
|
|
396
|
-
if (name === 'ID' || name === 'String') return { type: 'string' };
|
|
397
|
-
if (name === 'Int') return { type: 'integer' };
|
|
398
|
-
return { type: 'string', format: name ?? undefined };
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function isGraphQlNonNull(type: GraphQlIntrospectionTypeRef): boolean {
|
|
402
|
-
return type.kind === 'NON_NULL';
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function isNamedGraphQlType(type: GraphQlIntrospectionType): boolean {
|
|
406
|
-
return type.name !== undefined && type.name !== null;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function createGraphQlOperationMetadata(operation: GraphQlOperationDefinition): DataContractRecord {
|
|
410
|
-
const metadata = toMetadataRecord(operation.metadata);
|
|
411
|
-
metadata.kind = operation.kind;
|
|
412
|
-
if (operation.document !== undefined) metadata.document = operation.document;
|
|
413
|
-
if (operation.selectionPath !== undefined) metadata.selectionPath = operation.selectionPath;
|
|
414
|
-
return metadata;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function toMetadataRecord(value: DataContractValue | undefined): DataContractRecord {
|
|
418
|
-
if (!isDataContractRecord(value)) return {};
|
|
419
|
-
return { ...value };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function isDataContractRecord(value: DataContractValue | undefined): value is DataContractRecord {
|
|
423
|
-
return (
|
|
424
|
-
value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
425
|
-
);
|
|
426
|
-
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { DataSchema, DataSchemaRegistry } from '@ankhorage/contracts/data';
|
|
2
|
+
|
|
3
|
+
import { normalizeGraphQlOperationId } from './operation';
|
|
4
|
+
import type {
|
|
5
|
+
GraphQlIntrospectionInputValue,
|
|
6
|
+
GraphQlIntrospectionResult,
|
|
7
|
+
GraphQlIntrospectionType,
|
|
8
|
+
GraphQlIntrospectionTypeRef,
|
|
9
|
+
GraphQlOperationDefinition,
|
|
10
|
+
GraphQlOperationKind,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
const OBJECT_TYPE_KINDS = new Set(['OBJECT', 'INTERFACE', 'INPUT_OBJECT']);
|
|
14
|
+
|
|
15
|
+
export function normalizeGraphQlIntrospectionSchemas(
|
|
16
|
+
introspection: GraphQlIntrospectionResult | undefined,
|
|
17
|
+
): DataSchemaRegistry | undefined {
|
|
18
|
+
const types = introspection?.__schema?.types?.filter(isNamedGraphQlType) ?? [];
|
|
19
|
+
const entries = types.flatMap((type) => {
|
|
20
|
+
const { name } = type;
|
|
21
|
+
if (name === undefined || name === null || name.startsWith('__')) return [];
|
|
22
|
+
return [[name, normalizeGraphQlType(type)] as const];
|
|
23
|
+
});
|
|
24
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function normalizeGraphQlIntrospectionOperations(
|
|
28
|
+
introspection: GraphQlIntrospectionResult | undefined,
|
|
29
|
+
): readonly GraphQlOperationDefinition[] {
|
|
30
|
+
const schema = introspection?.__schema;
|
|
31
|
+
if (schema === undefined) return [];
|
|
32
|
+
|
|
33
|
+
const types = schema.types?.filter(isNamedGraphQlType) ?? [];
|
|
34
|
+
return [
|
|
35
|
+
...normalizeRootOperations('query', schema.queryType?.name, types),
|
|
36
|
+
...normalizeRootOperations('mutation', schema.mutationType?.name, types),
|
|
37
|
+
...normalizeRootOperations('subscription', schema.subscriptionType?.name, types),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeRootOperations(
|
|
42
|
+
kind: GraphQlOperationKind,
|
|
43
|
+
rootTypeName: string | null | undefined,
|
|
44
|
+
types: readonly GraphQlIntrospectionType[],
|
|
45
|
+
): readonly GraphQlOperationDefinition[] {
|
|
46
|
+
if (rootTypeName === undefined || rootTypeName === null) return [];
|
|
47
|
+
const rootType = types.find((type) => type.name === rootTypeName);
|
|
48
|
+
return (rootType?.fields ?? []).map((field) => ({
|
|
49
|
+
id: normalizeGraphQlOperationId(kind, field.name),
|
|
50
|
+
kind,
|
|
51
|
+
name: field.name,
|
|
52
|
+
description: field.description ?? undefined,
|
|
53
|
+
variables: normalizeGraphQlVariablesSchema(field.args ?? []),
|
|
54
|
+
response: normalizeGraphQlTypeRef(field.type),
|
|
55
|
+
selectionPath: `$.data.${field.name}`,
|
|
56
|
+
metadata: { rootType: rootTypeName, source: 'introspection' },
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeGraphQlVariablesSchema(
|
|
61
|
+
args: readonly GraphQlIntrospectionInputValue[],
|
|
62
|
+
): DataSchema {
|
|
63
|
+
const properties = Object.fromEntries(
|
|
64
|
+
args.map((arg) => [
|
|
65
|
+
arg.name,
|
|
66
|
+
{
|
|
67
|
+
...normalizeGraphQlTypeRef(arg.type),
|
|
68
|
+
description: arg.description ?? undefined,
|
|
69
|
+
default: arg.defaultValue ?? undefined,
|
|
70
|
+
},
|
|
71
|
+
]),
|
|
72
|
+
);
|
|
73
|
+
const required = args.filter((arg) => isGraphQlNonNull(arg.type)).map((arg) => arg.name);
|
|
74
|
+
return {
|
|
75
|
+
type: 'object',
|
|
76
|
+
required: required.length > 0 ? required : undefined,
|
|
77
|
+
properties,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeGraphQlType(type: GraphQlIntrospectionType): DataSchema {
|
|
82
|
+
if (OBJECT_TYPE_KINDS.has(type.kind)) return normalizeGraphQlObjectType(type);
|
|
83
|
+
if (type.kind === 'ENUM') return normalizeGraphQlEnumType(type);
|
|
84
|
+
if (type.kind === 'SCALAR') return normalizeGraphQlScalarType(type);
|
|
85
|
+
if (type.kind === 'UNION') return normalizeGraphQlUnionType(type);
|
|
86
|
+
return { title: type.name ?? undefined, description: type.description ?? undefined };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeGraphQlEnumType(type: GraphQlIntrospectionType): DataSchema {
|
|
90
|
+
return {
|
|
91
|
+
type: 'string',
|
|
92
|
+
title: type.name ?? undefined,
|
|
93
|
+
description: type.description ?? undefined,
|
|
94
|
+
enum: type.enumValues?.map((value) => value.name),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeGraphQlScalarType(type: GraphQlIntrospectionType): DataSchema {
|
|
99
|
+
return {
|
|
100
|
+
...normalizeGraphQlNamedScalar(type.name),
|
|
101
|
+
title: type.name ?? undefined,
|
|
102
|
+
description: type.description ?? undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeGraphQlUnionType(type: GraphQlIntrospectionType): DataSchema {
|
|
107
|
+
return {
|
|
108
|
+
title: type.name ?? undefined,
|
|
109
|
+
description: type.description ?? undefined,
|
|
110
|
+
anyOf: type.possibleTypes?.map(normalizeGraphQlTypeRef),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeGraphQlObjectType(type: GraphQlIntrospectionType): DataSchema {
|
|
115
|
+
const fields = type.kind === 'INPUT_OBJECT' ? type.inputFields : type.fields;
|
|
116
|
+
const entries = (fields ?? []).map(
|
|
117
|
+
(field) =>
|
|
118
|
+
[
|
|
119
|
+
field.name,
|
|
120
|
+
{
|
|
121
|
+
...normalizeGraphQlTypeRef(field.type),
|
|
122
|
+
description: field.description ?? undefined,
|
|
123
|
+
},
|
|
124
|
+
] as const,
|
|
125
|
+
);
|
|
126
|
+
const required = (fields ?? [])
|
|
127
|
+
.filter((field) => isGraphQlNonNull(field.type))
|
|
128
|
+
.map((field) => field.name);
|
|
129
|
+
return {
|
|
130
|
+
type: 'object',
|
|
131
|
+
title: type.name ?? undefined,
|
|
132
|
+
description: type.description ?? undefined,
|
|
133
|
+
required: required.length > 0 ? required : undefined,
|
|
134
|
+
properties: Object.fromEntries(entries),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeGraphQlTypeRef(type: GraphQlIntrospectionTypeRef): DataSchema {
|
|
139
|
+
if (type.kind === 'NON_NULL' && type.ofType !== undefined && type.ofType !== null) {
|
|
140
|
+
return { ...normalizeGraphQlTypeRef(type.ofType), nullable: false };
|
|
141
|
+
}
|
|
142
|
+
if (type.kind === 'LIST' && type.ofType !== undefined && type.ofType !== null) {
|
|
143
|
+
return { type: 'array', items: normalizeGraphQlTypeRef(type.ofType) };
|
|
144
|
+
}
|
|
145
|
+
if (type.kind === 'SCALAR') return normalizeGraphQlNamedScalar(type.name);
|
|
146
|
+
return type.name === undefined || type.name === null ? {} : { ref: { id: type.name } };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeGraphQlNamedScalar(name: string | null | undefined): DataSchema {
|
|
150
|
+
if (name === 'Boolean') return { type: 'boolean' };
|
|
151
|
+
if (name === 'Float') return { type: 'number' };
|
|
152
|
+
if (name === 'ID' || name === 'String') return { type: 'string' };
|
|
153
|
+
if (name === 'Int') return { type: 'integer' };
|
|
154
|
+
return { type: 'string', format: name ?? undefined };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isGraphQlNonNull(type: GraphQlIntrospectionTypeRef): boolean {
|
|
158
|
+
return type.kind === 'NON_NULL';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isNamedGraphQlType(type: GraphQlIntrospectionType): boolean {
|
|
162
|
+
return type.name !== undefined && type.name !== null;
|
|
163
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataContractValue,
|
|
3
|
+
DataOperationConfig,
|
|
4
|
+
DataOperationIntent,
|
|
5
|
+
DataOperationResponse,
|
|
6
|
+
OperationId,
|
|
7
|
+
} from '@ankhorage/contracts/data';
|
|
8
|
+
|
|
9
|
+
import type { GraphQlOperationDefinition, GraphQlOperationKind } from './types';
|
|
10
|
+
|
|
11
|
+
type DataContractRecord = Record<string, DataContractValue>;
|
|
12
|
+
|
|
13
|
+
export function normalizeGraphQlOperationId(kind: GraphQlOperationKind, name: string): OperationId {
|
|
14
|
+
const normalizedName = name
|
|
15
|
+
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
16
|
+
.replace(/^-+|-+$/g, '')
|
|
17
|
+
.toLowerCase();
|
|
18
|
+
return normalizedName.length > 0 ? `${kind}.${normalizedName}` : `${kind}.operation`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeGraphQlOperation(
|
|
22
|
+
operation: GraphQlOperationDefinition,
|
|
23
|
+
): DataOperationConfig {
|
|
24
|
+
const response: DataOperationResponse | undefined =
|
|
25
|
+
operation.response === undefined ? undefined : { schema: operation.response };
|
|
26
|
+
return {
|
|
27
|
+
id: operation.id,
|
|
28
|
+
endpointId: 'graphql',
|
|
29
|
+
name: operation.name,
|
|
30
|
+
description: operation.description,
|
|
31
|
+
protocol: 'graphql',
|
|
32
|
+
intent: mapGraphQlOperationKindToIntent(operation.kind),
|
|
33
|
+
request: { schema: operation.variables },
|
|
34
|
+
response,
|
|
35
|
+
metadata: createGraphQlOperationMetadata(operation),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mapGraphQlOperationKindToIntent(kind: GraphQlOperationKind): DataOperationIntent {
|
|
40
|
+
return kind === 'query' || kind === 'subscription' ? 'read' : 'action';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createGraphQlOperationMetadata(operation: GraphQlOperationDefinition): DataContractRecord {
|
|
44
|
+
return {
|
|
45
|
+
...toMetadataRecord(operation.metadata),
|
|
46
|
+
kind: operation.kind,
|
|
47
|
+
...(operation.document === undefined ? {} : { document: operation.document }),
|
|
48
|
+
...(operation.selectionPath === undefined ? {} : { selectionPath: operation.selectionPath }),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toMetadataRecord(value: DataContractValue | undefined): DataContractRecord {
|
|
53
|
+
return isDataContractRecord(value) ? { ...value } : {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isDataContractRecord(value: DataContractValue | undefined): value is DataContractRecord {
|
|
57
|
+
return (
|
|
58
|
+
value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
59
|
+
);
|
|
60
|
+
}
|