@ankhorage/data-sources 0.0.0 → 0.2.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 +15 -0
- package/README.md +12 -0
- package/dist/graphql/index.d.ts +83 -0
- package/dist/graphql/index.d.ts.map +1 -0
- package/dist/graphql/index.js +327 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/openapi/index.d.ts +89 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +330 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/rest/index.d.ts +44 -0
- package/dist/rest/index.d.ts.map +1 -0
- package/dist/rest/index.js +158 -0
- package/dist/rest/index.js.map +1 -0
- package/package.json +1 -1
- package/src/graphql/index.test.ts +202 -0
- package/src/graphql/index.ts +486 -0
- package/src/index.ts +4 -0
- package/src/openapi/index.test.ts +296 -0
- package/src/openapi/index.ts +576 -0
- package/src/rest/index.test.ts +237 -0
- package/src/rest/index.ts +243 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createGraphQlDataSource,
|
|
5
|
+
createGraphQlIntrospectionRequest,
|
|
6
|
+
type GraphQlIntrospectionResult,
|
|
7
|
+
normalizeGraphQlIntrospectionOperations,
|
|
8
|
+
normalizeGraphQlIntrospectionSchemas,
|
|
9
|
+
normalizeGraphQlOperationId,
|
|
10
|
+
} from './index';
|
|
11
|
+
|
|
12
|
+
function assertSerializable<TValue>(value: TValue): void {
|
|
13
|
+
expect(JSON.parse(JSON.stringify(value))).toEqual(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createIntrospection(): GraphQlIntrospectionResult {
|
|
17
|
+
return {
|
|
18
|
+
__schema: {
|
|
19
|
+
queryType: { name: 'Query' },
|
|
20
|
+
mutationType: { name: 'Mutation' },
|
|
21
|
+
types: [
|
|
22
|
+
{
|
|
23
|
+
kind: 'OBJECT',
|
|
24
|
+
name: 'Query',
|
|
25
|
+
fields: [
|
|
26
|
+
{
|
|
27
|
+
name: 'posts',
|
|
28
|
+
description: 'List posts.',
|
|
29
|
+
args: [
|
|
30
|
+
{
|
|
31
|
+
name: 'limit',
|
|
32
|
+
type: { kind: 'SCALAR', name: 'Int' },
|
|
33
|
+
defaultValue: '20',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
type: {
|
|
37
|
+
kind: 'LIST',
|
|
38
|
+
ofType: { kind: 'OBJECT', name: 'Post' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
kind: 'OBJECT',
|
|
45
|
+
name: 'Mutation',
|
|
46
|
+
fields: [
|
|
47
|
+
{
|
|
48
|
+
name: 'createPost',
|
|
49
|
+
args: [
|
|
50
|
+
{
|
|
51
|
+
name: 'title',
|
|
52
|
+
type: { kind: 'NON_NULL', ofType: { kind: 'SCALAR', name: 'String' } },
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
type: { kind: 'OBJECT', name: 'Post' },
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
kind: 'OBJECT',
|
|
61
|
+
name: 'Post',
|
|
62
|
+
fields: [
|
|
63
|
+
{
|
|
64
|
+
name: 'id',
|
|
65
|
+
type: { kind: 'NON_NULL', ofType: { kind: 'SCALAR', name: 'ID' } },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'title',
|
|
69
|
+
type: { kind: 'SCALAR', name: 'String' },
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('GraphQL introspection normalization', () => {
|
|
79
|
+
it('creates the standard introspection request', () => {
|
|
80
|
+
const request = createGraphQlIntrospectionRequest();
|
|
81
|
+
|
|
82
|
+
expect(request.operationName).toBe('AnkhorageGraphQlIntrospection');
|
|
83
|
+
expect(request.query).toContain('__schema');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('normalizes operation ids', () => {
|
|
87
|
+
expect(normalizeGraphQlOperationId('query', 'Posts')).toBe('query.posts');
|
|
88
|
+
expect(normalizeGraphQlOperationId('mutation', 'Create Post')).toBe('mutation.create-post');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('normalizes schemas from introspection results', () => {
|
|
92
|
+
const schemas = normalizeGraphQlIntrospectionSchemas(createIntrospection());
|
|
93
|
+
|
|
94
|
+
assertSerializable(schemas);
|
|
95
|
+
expect(schemas?.Post?.properties?.id?.type).toBe('string');
|
|
96
|
+
expect(schemas?.Post?.required).toContain('id');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('normalizes query and mutation operations from introspection results', () => {
|
|
100
|
+
const operations = normalizeGraphQlIntrospectionOperations(createIntrospection());
|
|
101
|
+
|
|
102
|
+
assertSerializable(operations);
|
|
103
|
+
expect(operations.map((operation) => operation.id)).toEqual([
|
|
104
|
+
'query.posts',
|
|
105
|
+
'mutation.createpost',
|
|
106
|
+
]);
|
|
107
|
+
expect(operations[0]?.variables?.properties?.limit?.type).toBe('integer');
|
|
108
|
+
expect(operations[1]?.variables?.required).toContain('title');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('creates a serializable GraphQL data source from introspection', () => {
|
|
112
|
+
const result = createGraphQlDataSource({
|
|
113
|
+
id: 'content-graphql',
|
|
114
|
+
endpointUrl: 'https://content.example.com/graphql',
|
|
115
|
+
introspection: createIntrospection(),
|
|
116
|
+
schemaVersion: '2026-05-19',
|
|
117
|
+
credential: {
|
|
118
|
+
id: 'content-token',
|
|
119
|
+
kind: 'bearer',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.ok).toBe(true);
|
|
124
|
+
if (result.ok) {
|
|
125
|
+
assertSerializable(result.data);
|
|
126
|
+
expect(result.data.kind).toBe('graphql');
|
|
127
|
+
expect(result.data.endpointUrl).toBe('https://content.example.com/graphql');
|
|
128
|
+
expect(result.data.introspection?.enabled).toBe(true);
|
|
129
|
+
expect(result.data.endpoints.graphql?.operations['query.posts']?.intent).toBe('read');
|
|
130
|
+
expect(result.data.endpoints.graphql?.operations['mutation.createpost']?.intent).toBe(
|
|
131
|
+
'action',
|
|
132
|
+
);
|
|
133
|
+
expect(
|
|
134
|
+
result.data.endpoints.graphql?.operations['mutation.createpost']?.request?.schema?.required,
|
|
135
|
+
).toContain('title');
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('creates a data source with manual GraphQL operations when introspection is unavailable', () => {
|
|
140
|
+
const result = createGraphQlDataSource({
|
|
141
|
+
id: 'manual-graphql',
|
|
142
|
+
endpointUrl: 'https://manual.example.com/graphql',
|
|
143
|
+
introspectionEnabled: false,
|
|
144
|
+
operations: [
|
|
145
|
+
{
|
|
146
|
+
id: 'query.viewer',
|
|
147
|
+
kind: 'query',
|
|
148
|
+
name: 'viewer',
|
|
149
|
+
document: 'query Viewer { viewer { id } }',
|
|
150
|
+
variables: { type: 'object' },
|
|
151
|
+
response: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
viewer: { type: 'object' },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
selectionPath: '$.data.viewer',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result.ok).toBe(true);
|
|
163
|
+
if (result.ok) {
|
|
164
|
+
const metadata = result.data.endpoints.graphql?.operations['query.viewer']?.metadata;
|
|
165
|
+
|
|
166
|
+
expect(metadata).toBeDefined();
|
|
167
|
+
if (metadata !== undefined && typeof metadata === 'object' && !Array.isArray(metadata)) {
|
|
168
|
+
expect(metadata.selectionPath).toBe('$.data.viewer');
|
|
169
|
+
}
|
|
170
|
+
expect(result.diagnostics).toEqual([]);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('reports invalid endpoint URLs and disabled-introspection mismatches', () => {
|
|
175
|
+
const result = createGraphQlDataSource({
|
|
176
|
+
id: 'broken-graphql',
|
|
177
|
+
endpointUrl: ' ',
|
|
178
|
+
introspectionEnabled: false,
|
|
179
|
+
introspection: createIntrospection(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(result.ok).toBe(false);
|
|
183
|
+
if (!result.ok) {
|
|
184
|
+
const paths = result.diagnostics.map((diagnostic) => diagnostic.path);
|
|
185
|
+
|
|
186
|
+
expect(paths).toContain('endpointUrl');
|
|
187
|
+
expect(paths).toContain('introspection');
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('reports missing introspection as info for manual operation workflows', () => {
|
|
192
|
+
const result = createGraphQlDataSource({
|
|
193
|
+
id: 'manual-graphql',
|
|
194
|
+
endpointUrl: 'https://manual.example.com/graphql',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result.ok).toBe(true);
|
|
198
|
+
if (result.ok) {
|
|
199
|
+
expect(result.diagnostics?.map((diagnostic) => diagnostic.code)).toContain('missing-schema');
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CredentialRef,
|
|
3
|
+
DataContractValue,
|
|
4
|
+
DataEndpointConfig,
|
|
5
|
+
DataOperationConfig,
|
|
6
|
+
DataOperationIntent,
|
|
7
|
+
DataOperationResponse,
|
|
8
|
+
DataSchema,
|
|
9
|
+
DataSchemaRegistry,
|
|
10
|
+
DataSourceDiagnostic,
|
|
11
|
+
DataSourceDiagnosticResult,
|
|
12
|
+
GraphQlDataSourceConfig,
|
|
13
|
+
OperationId,
|
|
14
|
+
} from '@ankhorage/contracts/data';
|
|
15
|
+
|
|
16
|
+
export const GRAPHQL_INTROSPECTION_QUERY = `query AnkhorageGraphQlIntrospection {
|
|
17
|
+
__schema {
|
|
18
|
+
queryType { name }
|
|
19
|
+
mutationType { name }
|
|
20
|
+
subscriptionType { name }
|
|
21
|
+
types {
|
|
22
|
+
kind
|
|
23
|
+
name
|
|
24
|
+
description
|
|
25
|
+
fields {
|
|
26
|
+
name
|
|
27
|
+
description
|
|
28
|
+
args {
|
|
29
|
+
name
|
|
30
|
+
description
|
|
31
|
+
type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
|
|
32
|
+
defaultValue
|
|
33
|
+
}
|
|
34
|
+
type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
|
|
35
|
+
}
|
|
36
|
+
inputFields {
|
|
37
|
+
name
|
|
38
|
+
description
|
|
39
|
+
type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
|
|
40
|
+
defaultValue
|
|
41
|
+
}
|
|
42
|
+
enumValues { name description }
|
|
43
|
+
possibleTypes { kind name }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}`;
|
|
47
|
+
|
|
48
|
+
export type GraphQlOperationKind = 'mutation' | 'query' | 'subscription';
|
|
49
|
+
|
|
50
|
+
type DataContractRecord = Record<string, DataContractValue>;
|
|
51
|
+
|
|
52
|
+
export interface GraphQlIntrospectionTypeRef {
|
|
53
|
+
readonly kind: string;
|
|
54
|
+
readonly name?: string | null;
|
|
55
|
+
readonly ofType?: GraphQlIntrospectionTypeRef | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface GraphQlIntrospectionInputValue {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly description?: string | null;
|
|
61
|
+
readonly type: GraphQlIntrospectionTypeRef;
|
|
62
|
+
readonly defaultValue?: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface GraphQlIntrospectionField {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly description?: string | null;
|
|
68
|
+
readonly args?: readonly GraphQlIntrospectionInputValue[] | null;
|
|
69
|
+
readonly type: GraphQlIntrospectionTypeRef;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface GraphQlIntrospectionEnumValue {
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly description?: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface GraphQlIntrospectionType {
|
|
78
|
+
readonly kind: string;
|
|
79
|
+
readonly name?: string | null;
|
|
80
|
+
readonly description?: string | null;
|
|
81
|
+
readonly fields?: readonly GraphQlIntrospectionField[] | null;
|
|
82
|
+
readonly inputFields?: readonly GraphQlIntrospectionInputValue[] | null;
|
|
83
|
+
readonly enumValues?: readonly GraphQlIntrospectionEnumValue[] | null;
|
|
84
|
+
readonly possibleTypes?: readonly GraphQlIntrospectionTypeRef[] | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface GraphQlIntrospectionSchema {
|
|
88
|
+
readonly queryType?: { readonly name?: string | null } | null;
|
|
89
|
+
readonly mutationType?: { readonly name?: string | null } | null;
|
|
90
|
+
readonly subscriptionType?: { readonly name?: string | null } | null;
|
|
91
|
+
readonly types?: readonly GraphQlIntrospectionType[] | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface GraphQlIntrospectionResult {
|
|
95
|
+
readonly __schema?: GraphQlIntrospectionSchema;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface GraphQlOperationDefinition {
|
|
99
|
+
readonly id: OperationId;
|
|
100
|
+
readonly kind: GraphQlOperationKind;
|
|
101
|
+
readonly name?: string;
|
|
102
|
+
readonly description?: string;
|
|
103
|
+
readonly document?: string;
|
|
104
|
+
readonly variables?: DataSchema;
|
|
105
|
+
readonly response?: DataSchema;
|
|
106
|
+
readonly selectionPath?: string;
|
|
107
|
+
readonly metadata?: DataContractValue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface GraphQlDataSourceDefinition {
|
|
111
|
+
readonly id: string;
|
|
112
|
+
readonly endpointUrl: string;
|
|
113
|
+
readonly name?: string;
|
|
114
|
+
readonly description?: string;
|
|
115
|
+
readonly credential?: CredentialRef;
|
|
116
|
+
readonly introspection?: GraphQlIntrospectionResult;
|
|
117
|
+
readonly introspectionEnabled?: boolean;
|
|
118
|
+
readonly schemaVersion?: string;
|
|
119
|
+
readonly operations?: readonly GraphQlOperationDefinition[];
|
|
120
|
+
readonly metadata?: DataContractValue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface GraphQlIntrospectionRequest {
|
|
124
|
+
readonly query: typeof GRAPHQL_INTROSPECTION_QUERY;
|
|
125
|
+
readonly operationName: 'AnkhorageGraphQlIntrospection';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function createGraphQlIntrospectionRequest(): GraphQlIntrospectionRequest {
|
|
129
|
+
return {
|
|
130
|
+
query: GRAPHQL_INTROSPECTION_QUERY,
|
|
131
|
+
operationName: 'AnkhorageGraphQlIntrospection',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function normalizeGraphQlOperationId(kind: GraphQlOperationKind, name: string): OperationId {
|
|
136
|
+
const normalizedName = name
|
|
137
|
+
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
138
|
+
.replace(/^-+|-+$/g, '')
|
|
139
|
+
.toLowerCase();
|
|
140
|
+
|
|
141
|
+
return normalizedName.length > 0 ? `${kind}.${normalizedName}` : `${kind}.operation`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createGraphQlDataSource(
|
|
145
|
+
definition: GraphQlDataSourceDefinition,
|
|
146
|
+
): DataSourceDiagnosticResult<GraphQlDataSourceConfig> {
|
|
147
|
+
const diagnostics = validateGraphQlDataSource(definition);
|
|
148
|
+
const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === 'error');
|
|
149
|
+
|
|
150
|
+
if (hasErrors) return { ok: false, diagnostics };
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
data: normalizeGraphQlDataSource(definition),
|
|
155
|
+
diagnostics,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function validateGraphQlDataSource(
|
|
160
|
+
definition: GraphQlDataSourceDefinition,
|
|
161
|
+
): readonly DataSourceDiagnostic[] {
|
|
162
|
+
const diagnostics: DataSourceDiagnostic[] = [];
|
|
163
|
+
|
|
164
|
+
if (definition.endpointUrl.trim().length === 0) {
|
|
165
|
+
diagnostics.push({
|
|
166
|
+
code: 'invalid-config',
|
|
167
|
+
dataSourceId: definition.id,
|
|
168
|
+
message: 'GraphQL data source requires a non-empty endpointUrl.',
|
|
169
|
+
path: 'endpointUrl',
|
|
170
|
+
severity: 'error',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (definition.introspectionEnabled === false && definition.introspection !== undefined) {
|
|
175
|
+
diagnostics.push({
|
|
176
|
+
code: 'invalid-config',
|
|
177
|
+
dataSourceId: definition.id,
|
|
178
|
+
message: 'GraphQL introspection data was provided while introspection is disabled.',
|
|
179
|
+
path: 'introspection',
|
|
180
|
+
severity: 'warning',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (definition.introspectionEnabled !== false && definition.introspection === undefined) {
|
|
185
|
+
diagnostics.push({
|
|
186
|
+
code: 'missing-schema',
|
|
187
|
+
dataSourceId: definition.id,
|
|
188
|
+
message:
|
|
189
|
+
'GraphQL introspection result was not provided. Manual operations can still be used.',
|
|
190
|
+
path: 'introspection',
|
|
191
|
+
severity: 'info',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return diagnostics;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function normalizeGraphQlDataSource(
|
|
199
|
+
definition: GraphQlDataSourceDefinition,
|
|
200
|
+
): GraphQlDataSourceConfig {
|
|
201
|
+
const schemas = normalizeGraphQlIntrospectionSchemas(definition.introspection);
|
|
202
|
+
const introspectionOperations = normalizeGraphQlIntrospectionOperations(definition.introspection);
|
|
203
|
+
const manualOperations = definition.operations ?? [];
|
|
204
|
+
const operations: Record<OperationId, DataOperationConfig> = {};
|
|
205
|
+
|
|
206
|
+
for (const operation of [...introspectionOperations, ...manualOperations]) {
|
|
207
|
+
operations[operation.id] = normalizeGraphQlOperation(operation);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const endpoint: DataEndpointConfig = {
|
|
211
|
+
id: 'graphql',
|
|
212
|
+
kind: 'graphql',
|
|
213
|
+
baseUrl: definition.endpointUrl,
|
|
214
|
+
credential: definition.credential,
|
|
215
|
+
operations,
|
|
216
|
+
metadata: {
|
|
217
|
+
source: 'graphql',
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
id: definition.id,
|
|
223
|
+
kind: 'graphql',
|
|
224
|
+
name: definition.name,
|
|
225
|
+
description: definition.description,
|
|
226
|
+
endpointUrl: definition.endpointUrl,
|
|
227
|
+
credential: definition.credential,
|
|
228
|
+
endpoints: {
|
|
229
|
+
graphql: endpoint,
|
|
230
|
+
},
|
|
231
|
+
schemas,
|
|
232
|
+
introspection: {
|
|
233
|
+
enabled: definition.introspectionEnabled ?? definition.introspection !== undefined,
|
|
234
|
+
schemaVersion: definition.schemaVersion,
|
|
235
|
+
},
|
|
236
|
+
metadata: definition.metadata,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function normalizeGraphQlIntrospectionSchemas(
|
|
241
|
+
introspection: GraphQlIntrospectionResult | undefined,
|
|
242
|
+
): DataSchemaRegistry | undefined {
|
|
243
|
+
const types = introspection?.__schema?.types?.filter(isNamedGraphQlType) ?? [];
|
|
244
|
+
if (types.length === 0) return undefined;
|
|
245
|
+
|
|
246
|
+
const schemas: Record<string, DataSchema> = {};
|
|
247
|
+
|
|
248
|
+
for (const type of types) {
|
|
249
|
+
if (type.name === undefined || type.name === null || type.name.startsWith('__')) continue;
|
|
250
|
+
schemas[type.name] = normalizeGraphQlType(type);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return schemas;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function normalizeGraphQlIntrospectionOperations(
|
|
257
|
+
introspection: GraphQlIntrospectionResult | undefined,
|
|
258
|
+
): readonly GraphQlOperationDefinition[] {
|
|
259
|
+
const schema = introspection?.__schema;
|
|
260
|
+
if (schema === undefined) return [];
|
|
261
|
+
|
|
262
|
+
const types = schema.types?.filter(isNamedGraphQlType) ?? [];
|
|
263
|
+
const operations: GraphQlOperationDefinition[] = [];
|
|
264
|
+
|
|
265
|
+
appendRootOperations(operations, 'query', schema.queryType?.name, types);
|
|
266
|
+
appendRootOperations(operations, 'mutation', schema.mutationType?.name, types);
|
|
267
|
+
appendRootOperations(operations, 'subscription', schema.subscriptionType?.name, types);
|
|
268
|
+
|
|
269
|
+
return operations;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function appendRootOperations(
|
|
273
|
+
operations: GraphQlOperationDefinition[],
|
|
274
|
+
kind: GraphQlOperationKind,
|
|
275
|
+
rootTypeName: string | null | undefined,
|
|
276
|
+
types: readonly GraphQlIntrospectionType[],
|
|
277
|
+
): void {
|
|
278
|
+
if (rootTypeName === undefined || rootTypeName === null) return;
|
|
279
|
+
|
|
280
|
+
const rootType = types.find((type) => type.name === rootTypeName);
|
|
281
|
+
for (const field of rootType?.fields ?? []) {
|
|
282
|
+
operations.push({
|
|
283
|
+
id: normalizeGraphQlOperationId(kind, field.name),
|
|
284
|
+
kind,
|
|
285
|
+
name: field.name,
|
|
286
|
+
description: field.description ?? undefined,
|
|
287
|
+
variables: normalizeGraphQlVariablesSchema(field.args ?? []),
|
|
288
|
+
response: normalizeGraphQlTypeRef(field.type),
|
|
289
|
+
selectionPath: `$.data.${field.name}`,
|
|
290
|
+
metadata: {
|
|
291
|
+
rootType: rootTypeName,
|
|
292
|
+
source: 'introspection',
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function normalizeGraphQlOperation(operation: GraphQlOperationDefinition): DataOperationConfig {
|
|
299
|
+
const response: DataOperationResponse | undefined =
|
|
300
|
+
operation.response === undefined
|
|
301
|
+
? undefined
|
|
302
|
+
: {
|
|
303
|
+
schema: operation.response,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
id: operation.id,
|
|
308
|
+
endpointId: 'graphql',
|
|
309
|
+
name: operation.name,
|
|
310
|
+
description: operation.description,
|
|
311
|
+
protocol: 'graphql',
|
|
312
|
+
intent: mapGraphQlOperationKindToIntent(operation.kind),
|
|
313
|
+
request: {
|
|
314
|
+
schema: operation.variables,
|
|
315
|
+
},
|
|
316
|
+
response,
|
|
317
|
+
metadata: createGraphQlOperationMetadata(operation),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function mapGraphQlOperationKindToIntent(kind: GraphQlOperationKind): DataOperationIntent {
|
|
322
|
+
if (kind === 'query' || kind === 'subscription') return 'read';
|
|
323
|
+
return 'action';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function normalizeGraphQlVariablesSchema(
|
|
327
|
+
args: readonly GraphQlIntrospectionInputValue[],
|
|
328
|
+
): DataSchema {
|
|
329
|
+
const properties: Record<string, DataSchema> = {};
|
|
330
|
+
const required: string[] = [];
|
|
331
|
+
|
|
332
|
+
for (const arg of args) {
|
|
333
|
+
properties[arg.name] = {
|
|
334
|
+
...normalizeGraphQlTypeRef(arg.type),
|
|
335
|
+
description: arg.description ?? undefined,
|
|
336
|
+
default: arg.defaultValue ?? undefined,
|
|
337
|
+
};
|
|
338
|
+
if (isGraphQlNonNull(arg.type)) required.push(arg.name);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
type: 'object',
|
|
343
|
+
required: required.length > 0 ? required : undefined,
|
|
344
|
+
properties,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function normalizeGraphQlType(type: GraphQlIntrospectionType): DataSchema {
|
|
349
|
+
if (type.kind === 'OBJECT' || type.kind === 'INTERFACE' || type.kind === 'INPUT_OBJECT') {
|
|
350
|
+
return normalizeGraphQlObjectType(type);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (type.kind === 'ENUM') {
|
|
354
|
+
return {
|
|
355
|
+
type: 'string',
|
|
356
|
+
title: type.name ?? undefined,
|
|
357
|
+
description: type.description ?? undefined,
|
|
358
|
+
enum: type.enumValues?.map((value) => value.name),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (type.kind === 'SCALAR') {
|
|
363
|
+
return {
|
|
364
|
+
...normalizeGraphQlNamedScalar(type.name),
|
|
365
|
+
title: type.name ?? undefined,
|
|
366
|
+
description: type.description ?? undefined,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (type.kind === 'UNION') {
|
|
371
|
+
return {
|
|
372
|
+
title: type.name ?? undefined,
|
|
373
|
+
description: type.description ?? undefined,
|
|
374
|
+
anyOf: type.possibleTypes?.map((possibleType) => normalizeGraphQlTypeRef(possibleType)),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
title: type.name ?? undefined,
|
|
380
|
+
description: type.description ?? undefined,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function normalizeGraphQlObjectType(type: GraphQlIntrospectionType): DataSchema {
|
|
385
|
+
const fields = type.kind === 'INPUT_OBJECT' ? type.inputFields : type.fields;
|
|
386
|
+
const properties: Record<string, DataSchema> = {};
|
|
387
|
+
const required: string[] = [];
|
|
388
|
+
|
|
389
|
+
for (const field of fields ?? []) {
|
|
390
|
+
const fieldType = 'type' in field ? field.type : undefined;
|
|
391
|
+
if (fieldType === undefined) continue;
|
|
392
|
+
|
|
393
|
+
properties[field.name] = {
|
|
394
|
+
...normalizeGraphQlTypeRef(fieldType),
|
|
395
|
+
description: field.description ?? undefined,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
if (isGraphQlNonNull(fieldType)) required.push(field.name);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
type: 'object',
|
|
403
|
+
title: type.name ?? undefined,
|
|
404
|
+
description: type.description ?? undefined,
|
|
405
|
+
required: required.length > 0 ? required : undefined,
|
|
406
|
+
properties,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function normalizeGraphQlTypeRef(type: GraphQlIntrospectionTypeRef): DataSchema {
|
|
411
|
+
if (type.kind === 'NON_NULL' && type.ofType !== undefined && type.ofType !== null) {
|
|
412
|
+
return {
|
|
413
|
+
...normalizeGraphQlTypeRef(type.ofType),
|
|
414
|
+
nullable: false,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (type.kind === 'LIST' && type.ofType !== undefined && type.ofType !== null) {
|
|
419
|
+
return {
|
|
420
|
+
type: 'array',
|
|
421
|
+
items: normalizeGraphQlTypeRef(type.ofType),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (type.kind === 'SCALAR') {
|
|
426
|
+
return normalizeGraphQlNamedScalar(type.name);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (type.name !== undefined && type.name !== null) {
|
|
430
|
+
return { ref: { id: type.name } };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return {};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function normalizeGraphQlNamedScalar(name: string | null | undefined): DataSchema {
|
|
437
|
+
if (name === 'Boolean') return { type: 'boolean' };
|
|
438
|
+
if (name === 'Float') return { type: 'number' };
|
|
439
|
+
if (name === 'ID' || name === 'String') return { type: 'string' };
|
|
440
|
+
if (name === 'Int') return { type: 'integer' };
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
type: 'string',
|
|
444
|
+
format: name ?? undefined,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function isGraphQlNonNull(type: GraphQlIntrospectionTypeRef): boolean {
|
|
449
|
+
return type.kind === 'NON_NULL';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function isNamedGraphQlType(type: GraphQlIntrospectionType): boolean {
|
|
453
|
+
return type.name !== undefined && type.name !== null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function createGraphQlOperationMetadata(operation: GraphQlOperationDefinition): DataContractRecord {
|
|
457
|
+
const metadata = toMetadataRecord(operation.metadata);
|
|
458
|
+
metadata.kind = operation.kind;
|
|
459
|
+
|
|
460
|
+
if (operation.document !== undefined) {
|
|
461
|
+
metadata.document = operation.document;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (operation.selectionPath !== undefined) {
|
|
465
|
+
metadata.selectionPath = operation.selectionPath;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return metadata;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function toMetadataRecord(value: DataContractValue | undefined): DataContractRecord {
|
|
472
|
+
if (!isDataContractRecord(value)) return {};
|
|
473
|
+
|
|
474
|
+
const metadata: DataContractRecord = {};
|
|
475
|
+
for (const [key, item] of Object.entries(value)) {
|
|
476
|
+
metadata[key] = item;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return metadata;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function isDataContractRecord(value: DataContractValue | undefined): value is DataContractRecord {
|
|
483
|
+
return (
|
|
484
|
+
value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
485
|
+
);
|
|
486
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { DataSourceConfig, DataSourceKind } from '@ankhorage/contracts/data';
|
|
2
2
|
|
|
3
|
+
export * from './graphql';
|
|
4
|
+
export * from './openapi';
|
|
5
|
+
export * from './rest';
|
|
6
|
+
|
|
3
7
|
export const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;
|
|
4
8
|
|
|
5
9
|
export const SUPPORTED_DATA_SOURCE_KINDS = [
|